Problem/Motivation

When a module update introduces a dependency on a new module and adds a service with a dependency on a service of the new module. The system breaks and the update hook can not be called because the service container can not be built.

Proposed resolution

In the update context remove all services that have unmet dependencies from the container. (Solution A)

Other options discussed.

  • Option A) in the UpdateServiceProvider remove all services with unmet dependencies. So that update hooks can be run at least.
  • Option B) in the UpdateKernel, add all available modules to the list before parent::initializeContainer().
  • Option C) in the UpdateKernel, add required modules to the module list before parent::initializeContainer(), even if they are not enabled yet. Side effects should be minimal, because the modules should be enabled after the update anyway.
  • Option D) merge the modules from core.extensions from the sync storage.
  • Option E) add an early update hook for enabling the new module before the services are scanned.
  • Option F) load a special services file according to the schema in the UpdateKernel, in which modules can declare old services.

Remaining tasks

  • agree on approach
  • implement

User interface changes

none

API changes

none

Data model changes

none

Release notes snippet

The update service container has services with unmet dependencies removed. This allows installed modules to be refactored to depend on new modules and install them in an update hook. See the corresponding change notice

CommentFileSizeAuthor
#126 2863986-126-D86.patch19.05 KBmaximpodorov
#114 2863986-4-114.patch18.61 KBalexpott
#114 111-114-interdiff.txt1.86 KBalexpott
#111 2863986-4-111.patch18.64 KBalexpott
#111 110-111-interdiff.txt4.8 KBalexpott
#110 2863986-4-110.patch17.9 KBalexpott
#110 106-110-interdiff.txt11.13 KBalexpott
#106 2863986-4-106.patch17.86 KBalexpott
#106 105-106-interdiff.txt5.88 KBalexpott
#105 2863986-4-105.patch16.62 KBalexpott
#105 102-105-interdiff.txt10.24 KBalexpott
#102 2863986-4-102.patch14.25 KBalexpott
#102 87-102-interdiff.txt4.69 KBalexpott
#102 2863986-4-test-only-102.patch12.94 KBalexpott
#102 87-test-only-102-interdiff.txt2.5 KBalexpott
#87 2863986-3-87.patch11.69 KBalexpott
#87 86-87-interdiff.txt1.2 KBalexpott
#86 2863986-3-86.patch11.72 KBalexpott
#86 77-86-interdiff.txt943 bytesalexpott
#77 2863986-77-8.7.x.patch11.7 KBpfrenssen
#66 2863986-66.interdiff.txt7.42 KBclaudiu.cristea
#66 2863986-66.patch14.45 KBclaudiu.cristea
#63 2863986-63.patch12.77 KBclaudiu.cristea
#63 2863986-63.interdiff.txt6.42 KBclaudiu.cristea
#62 2863986-62.interdiff.txt925 bytesclaudiu.cristea
#62 2863986-62.patch11.76 KBclaudiu.cristea
#49 2863986-2-49.patch11.75 KBalexpott
#49 48-49-interdiff.txt2.52 KBalexpott
#48 interdiff.txt3.7 KBpfrenssen
#48 2863986-48-test_only.patch9.61 KBpfrenssen
#48 2863986-48.patch12.14 KBpfrenssen
#46 2863986-46-test_only.patch9.61 KBpfrenssen
#46 2863986-46.patch15.84 KBpfrenssen
#44 interdiff-2863986-40-43.txt827 bytesbircher
#44 2863986-43.patch15.83 KBbircher
#41 interdiff-2863986-38-41.txt1.84 KBbircher
#41 2863986-41.patch12.05 KBbircher
#38 2863986-38.patch12.01 KBalexpott
#38 34-38-interdiff.txt1.29 KBalexpott
#34 2863986-34.patch12.01 KBbircher
#34 interdiff-2863986-31-34.txt8.72 KBbircher
#33 interdiff-2863986-31-33.txt8.6 KBbircher
#33 2863986-33.patch12.01 KBbircher
#31 interdiff-2863986-26-31.txt2.68 KBbircher
#31 2863986-31.patch10.15 KBbircher
#27 interdiff-2863986-26-27.txt4.44 KBbircher
#27 2863986-27.patch10.65 KBbircher
#27 interdiff-2863986-26-27.txt4.44 KBbircher
#27 2863986-27.patch10.65 KBbircher
#26 interdiff-2863986-25-26.txt755 bytesbircher
#26 2863986-26.patch10.09 KBbircher
#25 interdiff-2863986-23-25-alias.txt659 bytesbircher
#25 2863986-25-alias.patch9.84 KBbircher
#23 2863986-23.patch9.72 KBAda Hernandez
#23 interdiff-21-23.txt615 bytesAda Hernandez
#21 interdiff-2863986-19-21.txt7.91 KBbircher
#21 2863986-21.patch9.76 KBbircher
#19 interdiff-2863986-16-18.txt1.7 KBbircher
#19 2863986-18.patch6.98 KBbircher
#17 interdiff-2863986-15-16.txt2.2 KBbircher
#17 2863986-16.patch6.95 KBbircher
#15 interdiff-2863986-2-15.txt1.16 KBbircher
#15 2863986-15.patch6.77 KBbircher
#2 interdiff-2863986-2-workaround.txt1.02 KBbircher
#2 2863986-2-workaround.patch6.59 KBbircher
#2 2863986-2.patch5.56 KBbircher
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

bircher created an issue. See original summary.

bircher’s picture

Attached is a patch with a test that demonstrates the problem.

And a workaround that developers have to do now (and leave in code forever or break the update path)

I experienced this problem in #2859628: Dependancy on non-existent service and the workaround is used for Config Split beta2 -> beta4.

webflo’s picture

Status: Active » Needs review
alexpott’s picture

Looking at the issue summary and thinking about efforts we were making when we were trying to support Drupal 7 to Drupal 8 via update.php - I'm not sure option B is possible. If we add all the services to the service container. Then one of these services might make a change, for example an on demand creation of a table, but the module providing the service might not end up installed. That might be really tricky to get back from.

I think @dawehner might bring up the idea of pre-update hooks where this type of the can be resolved before there is a container that can get in the way. The problem there specifically for module install is that we have hook_install where people use the container.

webflo’s picture

Update path in Acquia Content Hub is broken because of it. #2851258: Add diff module to the composer.json

dawehner’s picture

We had that kind of discussions/explorations with #2547507: Consider adding hook_update_early_N() support before

The last submitted patch, 2: 2863986-2.patch, failed testing.

webflo’s picture

Issue summary: View changes

Added option C.

dawehner’s picture

One possible workaround would be to do a module exists / lookup in the core.extension configuration inside a ServiceProvider class. If your new dependency doesn't exist, ignore the services, otherwise continue on.

It feels like your module should be written against those problems in the first place, so its not necessarily the worst when it would break and provide you a helpful exception message.

bircher’s picture

Issue summary: View changes

So adding all services simply doesn't work. A quick test confirms that this breaks in all sorts of spectacular ways.

I added the DX tag to this because while it is possible to have optional dependencies via setter injection, the constructor injector pattern is far more prevalent.
And I think both setter injection and the service provider workaround are things that live in the code active forever and are unnecessary for fresh installations of the module, just because there is no way to upgrade drupal in that situation.

I think merging the enabled list from core.extensions from the sync storage could help, since then we don't need to resolve dependencies and discover modules, and while developing one would enable the module before adding the service, and during the deployment the sync storage is updated together with the updated code.

alexpott’s picture

Another option is for people to be able to version their services.yml file. So you build with the old services and then once the new dependency is enabled you switch to the new services.yml file. How might this work?

Timeline:

  1. ModuleA is installed and has ServiceA declared in services.yml
  2. ModuleA is update to depend on ModuleB and ServiceA depends on ServiceB
  3. modulea_update_8001 is added to do the update. It would crash because now modulea.services.yml has a serivce dependent on something from an uninstalled module
  4. But ModuleA has a modulea.services.8001.yml that it uses instead whilst (and before) running modulea_update_8001.
  5. Immediately after modulea_update_8001 the container is rebuilt and because the schema is not 8001 the service builder uses updated modulea.services.yml and not the special 8001 file.
alexpott’s picture

It a lot of instances the modulea.services.UPDATE_N.yml file might just not include the affected service because it is not used at all during the update.

dawehner’s picture

@bircher
Do you want to summarize the discussion we had at that day?

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

bircher’s picture

attempt at solution A

bircher’s picture

Issue summary: View changes

So the patch is green and good to be reviewed and committed.

I went with solution A because it is the minimal viable solution to the problem and being able to boot the kernel is a prerequisite to other solutions.

We also discussed automatically enabling all required modules, but I think it is ok to ask module maintainers to enable the new dependency in an update hook.
The solution in #11 would probably also work but also mandates keeping additional files around that are no longer needed. This would be ok if we would enable the dependencies automatically but if we need the update hook anyway, we might as well only do that.

bircher’s picture

as discussed with gambry on irc, check to see if the service assertion can work, otherwise we can also drop it as the module is installed we know the service will be there.

Status: Needs review » Needs work

The last submitted patch, 17: 2863986-16.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

bircher’s picture

Status: Needs work » Needs review
FileSize
6.98 KB
1.7 KB

if at first you don't succeed...

gambry’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/system/src/Tests/Update/UpdatePathNewDependencyTest.php
    @@ -0,0 +1,46 @@
    +/**
    + * @group system
    

    Missing short description.

  2. +++ b/core/modules/system/src/Tests/Update/UpdatePathNewDependencyTest.php
    @@ -0,0 +1,46 @@
    +    // No database required,
    

    Comma at the end of the comment, although a better copy (or not comment at all) I think will help readability.

  3. +++ b/core/modules/system/src/Tests/Update/UpdatePathNewDependencyTest.php
    @@ -0,0 +1,46 @@
    +    $this->rebuildAll();
    +    $this->drupalGet('<front>');
    

    Assertion passes locally without this two lines.

  4. +++ b/core/modules/system/tests/modules/new_dependency_test/new_dependency_test.install
    @@ -0,0 +1,13 @@
    + * @file
    

    Missing short description.

  5. +++ b/core/modules/system/tests/modules/new_dependency_test/src/DependentService.php
    @@ -0,0 +1,22 @@
    +class DependentService {
    

    This class and its method needs a bit of love docblock wise (Class doc, constructor argument, hello() method).

  6. +++ b/core/modules/system/tests/modules/new_dependency_test_with_service/src/NewService.php
    @@ -0,0 +1,14 @@
    +class NewService {
    

    Same as above, a bit of doc-love is needed.

Solution A is simple and effective, although I must say I was a big fan of C.
I don't see scenario where Solution A won't work. The only "critical" service which must never fail here is module_installer itself and its arguments, but as core services they will never depend by any module.

Will the container rebuild automatically after the update process is complete?
(I know it does for post_updates, not sure about updates).

bircher’s picture

Issue summary: View changes
Status: Needs work » Needs review
FileSize
9.76 KB
7.91 KB

Thanks a lot for the in-depth review.
Attached a new version with more comments and with recursive service removal, as services could depend on removed services.

Yes option C sounds nice too. We were even discussing automatically enabling required modules. But we would have to special-case profiles again because it is currently ok to disable modules a profile depends on. (#820054: Add support for recommends[] and fix install profile dependency special casing is also blocking other things.)
The problem, however, is that we use a service to determine the available modules and their dependencies, to use services we need to be able to have the service container ready which can't have services with unmet dependencies even if you are not using them.
So maybe there is a smarter way to have a running container with missing dependencies, but getting the container to run with less services to enable the new modules seems to me to be a big win as compared to not having a solution at all like now.

The workaround for this problem is to remove the the modules service in its own ServiceProvider, but every module needs to do that by themselves now and this is something core could do.

gambry’s picture

Assigned: bircher » Unassigned
Status: Needs review » Needs work

@bircher thanks for your work. Just a minor issue:

+++ b/core/modules/system/tests/modules/new_dependency_test/new_dependency_test.install
@@ -0,0 +1,14 @@
+ * Enable the config_filter module.
...
+  \Drupal::getContainer()->get('module_installer')->install(['new_dependency_test_with_service']);

"Enable the new_dependency_test_with_service module."

However

I've done a manual test and running updates from the Web UI works, but through drush updb doesn't.
It looks to me like the container is loaded without UpdateServiceProvider alteration. I get the 'The service [error]
"new_dependency_test.dependent" has a dependency on a non-existent service "new_dependency_test_with_service.service".
' message before any Drupal\Core\Update class is loaded, so I presume drush use some kind of cache OR loads the full container (i.e. bootstrap the full drupal) before starting the update process. I'm not 100% familiar with drush code so I'm just guessing.

Is that just me (drush 8.1.3)?

Ada Hernandez’s picture

Status: Needs work » Needs review
FileSize
615 bytes
9.72 KB

@gambry the minor issue is fixed.

dawehner’s picture

+++ b/core/lib/Drupal/Core/Update/UpdateServiceProvider.php
@@ -32,6 +32,37 @@ public function alter(ContainerBuilder $container) {
+    if ($removed) {
+      // Recursively remove undefined services if a service had to be removed.
+      $this->removeUndefinedServices($container);
+    }

❓I'm wondering, do we need some kind of recursion protection here? Do we might, for some weird reason, end up in a loop, like when maybe container aliases are involved?

bircher’s picture

FileSize
9.84 KB
659 bytes

RE #24: Actually the recursion could also be a do-while loop as such. We go over all service definitions and remove the ones that have unmet dependencies. then do that as long as there is something to remove. It is essentially looping over an array and potentially removing elements from it repeating it if something was removed. The only way this could become a loop would be if \Symfony\Component\DependencyInjection\ContainerBuilder::removeDefinition would add a definition.

But thinking about aliases actually reveals that we also have to take care of them. Attached is the proof.

bircher’s picture

Attached the fix for aliases.

bircher’s picture

Ok attached is another solution (C) that could work. I had some problems though with it so maybe (A) is safer ie patch from #26.

As for drush, yes once we decide which way to go with this we can look into making drush use a similar trick. But I don't know either at the moment which way.

Alternatively and this is also something we discussed is that the code here in this patch could go straight into the DrupalKernel, ie we always add the services of dependent modules and add them to the container modules too. But I don't know if there would be reasons for not doing that. and it would become a bigger scope issue.

The last submitted patch, 25: 2863986-25-alias.patch, failed testing. View results

The last submitted patch, 27: 2863986-27.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 27: 2863986-27.patch, failed testing. View results

bircher’s picture

Status: Needs work » Needs review
FileSize
10.15 KB
2.68 KB

Ok, so option C does break some other unrelated test it seems. I don't have the time right now to see if it is a false negative or what is wrong with that other test or the patch.

Attached is a patch for option A with a do-while instead of the recursion.

gambry’s picture

Shouldn't we allow services with missing optional dependencies?
We have some of them in core, for instance core.form_builder, besides using optional arguments can also be a solution developers may apply to temporary have a working container until the new dependent module is enabled. And we don't want to remove those valid services.

If the answer is yes then we should also test we don't remove them.

bircher’s picture

bircher’s picture

FileSize
8.72 KB
12.01 KB

I just noticed that I added the optional service to the wrong class....

The last submitted patch, 33: 2863986-33.patch, failed testing. View results

The last submitted patch, 33: 2863986-33.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 34: 2863986-34.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

alexpott’s picture

Status: Needs work » Needs review
FileSize
1.29 KB
12.01 KB

A couple of fixes to coding standards and namespaces. The UpdatePathNewDependencyTest is still failing because of:

    There was 1 error:

    1)
    Drupal\Tests\system\Functional\Update\UpdatePathNewDependencyTest::testUpdateNewDependency
    Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException:
    The service "new_dependency_test.dependent" has a dependency on a
    non-existent service "new_dependency_test_with_service.service".

    /Volumes/devdisk/dev/sites/drupal8alt.dev/vendor/symfony/dependency-injection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php:58
    /Volumes/devdisk/dev/sites/drupal8alt.dev/vendor/symfony/dependency-injection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php:42
    /Volumes/devdisk/dev/sites/drupal8alt.dev/vendor/symfony/dependency-injection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php:36
    /Volumes/devdisk/dev/sites/drupal8alt.dev/vendor/symfony/dependency-injection/Compiler/Compiler.php:120
    /Volumes/devdisk/dev/sites/drupal8alt.dev/vendor/symfony/dependency-injection/ContainerBuilder.php:573
    /Volumes/devdisk/dev/sites/drupal8alt.dev/core/lib/Drupal/Core/DrupalKernel.php:1307
    /Volumes/devdisk/dev/sites/drupal8alt.dev/core/lib/Drupal/Core/DrupalKernel.php:884
    /Volumes/devdisk/dev/sites/drupal8alt.dev/core/lib/Drupal/Core/DrupalKernel.php:1166
    /Volumes/devdisk/dev/sites/drupal8alt.dev/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php:179
    /Volumes/devdisk/dev/sites/drupal8alt.dev/core/modules/system/tests/src/Functional/Update/UpdatePathNewDependencyTest.php:39

Status: Needs review » Needs work

The last submitted patch, 38: 2863986-38.patch, failed testing. View results

alexpott’s picture

I think the fail in #38 is happening because we rebuild the container after faking the module install and this picks up the new service. Which is broken with the regular non update container because it is missing

bircher’s picture

Status: Needs work » Needs review
Issue tags: +Vienna2017
FileSize
12.05 KB
1.84 KB

Yes because in the previous UpdatePathTestBase the container was the update container. So now we can just test that it properly fails.

Status: Needs review » Needs work

The last submitted patch, 41: 2863986-41.patch, failed testing. View results

alexpott’s picture

+++ b/core/modules/system/tests/modules/new_dependency_test/new_dependency_test.install
@@ -0,0 +1,14 @@
+  debug('Running the new_dependency_test_update_8001.');

Debug left in which is causing the test to fail. Otherwise the test is looking good.

I think this approach should make it much easier for services to change their dependencies. Nice work.

bircher’s picture

So debug statements lead to failures now..

bircher’s picture

Status: Needs work » Needs review

:) Thanks, we cross-posted!

pfrenssen’s picture

pfrenssen’s picture

Status: Needs review » Needs work
+++ b/core/modules/path/src/Form/PathFormBase.php
@@ -192,7 +192,7 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
-      $form_state->setErrorByName('source', t("Either the path '@link_path' is invalid or you do not have access to it.", ['@link_path' => $source]));
+      $form_state->setErrorByName('source', t("The path '@link_path' is either invalid or you do not have access to it.", ['@link_path' => $source]));

Hmm why are we changing these strings? This wasn't present in earlier versions of the patch, it was introduced in #44.

It looks like this is accidentally reverting #2911221: Ungrammatical validation message for paths.

pfrenssen’s picture

Status: Needs work » Needs review
FileSize
12.14 KB
9.61 KB
3.7 KB

Reverted those accidental changes.

alexpott’s picture

Slightly re-worked some comments and also removed the method and inlined the functionality. The method doesn't really add anything here apart from boilerplate.

The last submitted patch, 46: 2863986-46-test_only.patch, failed testing. View results

dawehner’s picture

+++ b/core/lib/Drupal/Core/Update/UpdateServiceProvider.php
@@ -32,6 +33,32 @@ public function alter(ContainerBuilder $container) {
+          if ($argument instanceof Reference) {
+            if (!$container->has((string) $argument) && $argument->getInvalidBehavior() === ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {
+              // If the container does not have the argument and would throw an
+              // exception, remove the service.
+              $container->removeDefinition($key);
+            }
+          }

Is there a reason we don't handle parameters? Maybe we should handle those in a follow up?

The last submitted patch, 48: 2863986-48-test_only.patch, failed testing. View results

claudiu.cristea’s picture

I manually tested #49 with update.php, through web, and is fixing the problem. But with drush 9 still fails.

bircher’s picture

Thanks for re-rolling the patch! Wow a lot of activity in a short amount of time!

And yes it currently only works with update.php because drush does not use the update kernel or update service provider. Would not be such a complicated change to drush I think.

pfrenssen’s picture

The issue in Drush to use the UpdateKernel for the updatedb command is https://github.com/drush-ops/drush/issues/3193

claudiu.cristea’s picture

Drush related issue https://github.com/drush-ops/drush/issues/3193

EDIT: Sorry, I had an old version of the page loaded in browser and I missed @pfrenssen comment.

pfrenssen’s picture

This looks RTBC to me, but I am not sure about @dawehner's comment:

Is there a reason we don't handle parameters? Maybe we should handle those in a follow up?

The arguments are (AFAIK) either references to other services or strings, and this fix only is concerned about references to non-existing services. What kind of parameters do you mean?

pfrenssen’s picture

dawehner’s picture

What kind of parameters do you mean?

Well for example '%user.tempstore.expire%'. When I talked with @alexpott about this, he argued that it might be right to do this in a follow up, but well I think its essentially the same problem, we just don't run into it that often with parameters.

sardara’s picture

+++ b/core/lib/Drupal/Core/Update/UpdateServiceProvider.php
@@ -32,6 +33,32 @@ public function alter(ContainerBuilder $container) {
+    // The kernel cannot be booted if the container such services. This allows

A verb is missing here I think.

claudiu.cristea’s picture

The Drush version could be reviewed here https://github.com/drush-ops/drush/pull/3206

claudiu.cristea’s picture

Version: 8.5.x-dev » 8.4.x-dev
Issue tags: -DevDaysSeville, -Vienna2017
FileSize
11.76 KB
925 bytes

Fixed #60. This is a bug, so it's 8.4.x.

claudiu.cristea’s picture

FileSize
6.42 KB
12.77 KB

@dawehner, something like this?

claudiu.cristea’s picture

Priority: Normal » Major

This breaks database updates when conditions described in "Problem/Motivation" of the issue summary are met. For this reason, this is at least Major (not sure if it's not Critical).

pfrenssen’s picture

Yeah this is at least major bordering on critical. When this problem occurs you need specialist knowledge to get a working system again. When services are missing during the building of the dependency injection container you cannot bootstrap any more. This means that even the most basic tools that people rely on to restore a broken system (like clearing the cache) suddenly don't work any more.

claudiu.cristea’s picture

FileSize
14.45 KB
7.42 KB

The parameters part from #64 was wrong because the service referring missing parameter dependencies should be removed. This patch is fixing this, however the test will fail and I cannot figure why. It happens during running updates.

claudiu.cristea’s picture

The problem is that the parameters (as parameter bag) are used during compiling the container, in pass ResolveParameterPlaceHoldersPass and... buuum! I have no experience on how to resolve this. Probably adding a new pass before? No idea.

Status: Needs review » Needs work

The last submitted patch, 66: 2863986-66.patch, failed testing. View results

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.4 was released on January 3, 2018 and is the final full bugfix release for the Drupal 8.4.x series. Drupal 8.4.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.5.0 on March 7, 2018. (Drupal 8.5.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.5.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

mpp’s picture

Does option A imply that one needs to run database updates before the site can be set to maintenance mode using drush sset?

pfrenssen’s picture

@mpp you can avoid that problem by putting the site in maintenance mode at the very start of the process, before the new code is deployed.

mpp’s picture

Good point, thanks @pfrenssen.

mxr576’s picture

Anybody can help out @claudiu.cristea with an answer for #67? The reported problem can easily break production sites where re-install may not be an option to fix this.

Edited:
Of course nobody updated modules on production with Composer and everybody has backups, but this whole problem makes deployments more problematic because a newly added module (service dependency) have to be deployed and enabled in (production) environment otherwise the site becomes broken. If we could run "drush en missing_dependency" at least in these case that would be a huge help.

Or maybe we would need a solution like registry_rebuild was for Drupal 7 in Drupal 8 too.

gambry’s picture

Can we do the parameters work in a follow-up, and keeping this issue for the arguments only?
So #62 as latest patch?

I'm concious this is a pretty nasty issue and by fixing at least the arguments bit will be a massive benefit.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.6 was released on August 1, 2018 and is the final bugfix release for the Drupal 8.5.x series. Drupal 8.5.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.6.0 on September 5, 2018. (Drupal 8.6.0-rc1 is available for testing.)

Bug reports should be targeted against the 8.6.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

seanB’s picture

+1 for committing #62 and creating a followup for the parameters.

Still found an issue with #62:

+++ b/core/lib/Drupal/Core/Update/UpdateServiceProvider.php
@@ -32,6 +33,33 @@ public function alter(ContainerBuilder $container) {
+            if (!$container->has((string) $argument) && $argument->getInvalidBehavior() === ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {
+              // If the container does not have the argument and would throw an
+              // exception, remove the service.
+              $container->removeDefinition($key);

This also seems to remove decorated services using ["@bar.inner"] notation. Which causes other services to give the same error again.

pfrenssen’s picture

Patch doesn't apply to 8.7.x any more since #3006086: update.php should not process path aliases got in. Here's a reroll of the patch from #63 for 8.7.x.

alexpott’s picture

Spent the day co-working with @justafish who was trying to refactor some services out of a custom module to another custom module and fighting this was extremely painful. I really think we have to do something here. This patch didn't solve the problems because after applying this patch they saw
The service "drush.command.services" has a dependency on a non-existent service "config_split.commands".
During updatedb which I think was because a cache rebuild prior to running updatedb had failed.

You could assume that the cache rebuild prior to running updatedb is a bad thing but this is more akin to the cache free environments core runs it's updates in.

It's very tricky to know what to recommend people to do.

claudiu.cristea’s picture

@alexpott, I have a similar case but mine is more complex. The service that I split in the new module (let's say 'service_foo') is called in this hook implementation:

function mymodule_entity_type_alter(...) {
   ...
   \Drupal::service('service_foo');
   ...
}

On update, when the container is build, specifically the date.formatter service, its constructor is doing this:

  public function __construct(EntityManagerInterface $entity_manager, ...) {
    $this->dateFormatStorage = $entity_manager->getStorage('date_format');
    ...
  }

But this leads to building the entity type definitions and consequently the hook_entity_type_alter() implementations are called. Booom!!!

I think building the entity type definitions in a service instantiation is wrong for many reasons. I encounter this problem also with other cases but I cannot recall.

justafish’s picture

As @bircher alluded to in #10 you can make your services optional, so I just wanted to provide an explicit example in case anyone runs across this looking for a solution now.

Bark service originally provided by labrador:

services:
  labrador.some_handler:
    class: Drupal\labrador\Controller\SomeHandler
    arguments: [ '@labrador.bark' ]
  public function __construct(Bark $bark) {
    $this->bark = $bark;
  }

Moving the Bark service to a new dog module:

services:
  labrador.some_handler:
    class: Drupal\labrador\Controller\SomeHandler
    calls:
      - [setBark, ['@?dog.bark']]

Remove the constructor argument/call and add:

  public function setBark(Bark $bark) {
    $this->bark = $bark;
  }
achap’s picture

Just to add some feedback. The patch in number 63 is still giving me the message like this:

The service "drush.command.services" has a dependency on a non-existent service "config_split.commands".

I ended up having to work around it by using the solution posted in #80.

bradjones1’s picture

nevergone’s picture

Version: 8.6.x-dev » 8.7.x-dev

Will you be preparing for 8.7?

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

chr.fritsch’s picture

I also ran into this while updating blazy from 1->2 at the same time like media_entity to media in core.

The blazy 2 service (@blazy.oembed) depends on a media in core service (@media.oembed.resource_fetcher). But before updb, the media service is not available. The media module will be enabled during the update.

Because of a container rebuild before updb starts, I get "The service "blazy.oembed" has a dependency on a non-existent service "media.oembed.resource_fetcher"."

The patch from #77 allows me to run the update successfully.

alexpott’s picture

Fixed the test and used a more recent dump for a quicker test. It some ways it'd be great to not have to make this a legacy test and not use a db dump at at all. We'd need to re-engineer update path test base to support tests that do a regular install first. I'll open an issue for this as I've hit the same issue in #3063734: Support uninstalling the module providing the update hook in hook_update_N

alexpott’s picture

alexpott’s picture

I think we should consider moving forward with this approach because it solves real world problems. Yes it doesn't solve every problem we have but I'm not sure that's possible. For example, the issue that @claudiu.cristea alludes with using a new service in hook_entity_type_alter() is that this will be called in early updates. There's no real way to work around that. So the implement probably needs to be adjusted to do something like \Drupal::hasService() first.

chr.fritsch’s picture

Priority: Major » Critical
Status: Needs review » Reviewed & tested by the community

As mentioned in #85 this allows us to run the blazy update at the same time like the media_entity -> media update. So I am marking this as RTBC.

I am also raising the priority to critical because without this patch we have no chance to do the update. I would also vote for a backport to 8.7.x.

The last submitted patch, 86: 2863986-3-86.patch, failed testing. View results

JvE’s picture

pfrenssen’s picture

I agree that it would be good to have this in, even though it does not fix all edge cases. The main problem that most people encounter is fixed and it can be very puzzling to figure this out. We have been using the various versions of this patch in production already since 2017.

I reviewed the 2 recent interdiffs and they look good to me. RTBC +1.

bircher’s picture

+1 for getting this in.

We now don't do arguments and I think we can do them in a followup if it really comes up as a big problem (#51 #59)
The reason for which I don't think arguments should hold this up is that:
a) On one hand it is less likely that a service in an existing module depends on parameters defined in a new module without also using one of the services of the new module. (Therefore, the dependency on the new service would make the offending one disappear for the update)
b) And on the other hand parameters are typically overwritten in a site specific services.yml and if they are then the problem is solved for them. So this happens to be a relatively simple workaround for a deployment that breaks in this specific case that is not covered by a)

I am also fine with hooks having to do a \Drupal::hasService() before calling \Drupal::service(). I am not sure where the best place would be to document that though.

catch’s picture

Status: Reviewed & tested by the community » Fixed

For a moment I was thinking we might end up hiding errors here, but this only happens during updates so if someone really has defined a broken service definition it'll still be reported.

Can't think of anything else we can do here, and agreed we only need to improve things here rather than fix every possible scenario.

Committed 8f430db and pushed to 8.8.x. Thanks!

  • catch committed 8f430db on 8.8.x
    Issue #2863986 by bircher, pfrenssen, alexpott, claudiu.cristea, Adita,...
chr.fritsch’s picture

Version: 8.8.x-dev » 8.7.x-dev
Status: Fixed » Reviewed & tested by the community

Talked shortly with @catch in slack and he sees a chance to commit this to 8.7.x after the next patch release.

bradjones1’s picture

Thank you everyone who worked so hard on this; I have run into this issue quite often on long-standing projects and it's nice to not need to add awkward checks to event subscribers and similar things just to work around this item.

Even though this doesn't create an API change, would it be helpful to have a Change Record in order to describe to developers how this works, and if there is any action they need to take in update hooks in order to take advantage of this? From a read of the commit I'm not 100% sure on what the proper workflow is.

I'd be happy to help write one if I can get confirmation on the items above. Thanks!

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

@bradjones1 there's nothing for a developer to do. If the added service is not use in update hooks and has missing dependencies at the beginning but not at the end of the updates then everything will happily work. There are a number of situations this patch does not fix though so writing a change record is going to be tricky because there's nothing for a developer to change and covering all the edge cases is hard.

Unfortunately though we need to roll this back as this breaks service decoration. It'll remove a service which is using the .inner service as an argument see https://symfony.com/doc/current/service_container/service_decoration.html for how service decoration works and how it creates a pseudo service of .inner.

alexpott’s picture

+++ b/core/lib/Drupal/Core/Update/UpdateServiceProvider.php
@@ -52,6 +53,33 @@ public function alter(ContainerBuilder $container) {
+            if (!$container->has((string) $argument) && $argument->getInvalidBehavior() === ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {
+              // If the container does not have the argument and would throw an
+              // exception, remove the service.
+              $container->removeDefinition($key);
+            }

This needs to check if the definition is decorating a service and do special logic.

  • catch committed fc0331b on 8.8.x
    Revert "Issue #2863986 by bircher, pfrenssen, alexpott, claudiu.cristea...
catch’s picture

Version: 8.7.x-dev » 8.8.x-dev

Reverted, back to 8.8.x

alexpott’s picture

Here's a patch that fixes the issue with using decorated services during update and adds a test.

The last submitted patch, 102: 2863986-4-test-only-102.patch, failed testing. View results

bircher’s picture

Oh right! Yes that is a very good point.
I am happy that we now also test services that remain.
The following comments are more of a nit-picky sort, but they are posted here because I stumbled on them a bit when reviewing.

  1. +++ b/core/modules/system/tests/modules/new_dependency_test/new_dependency_test.install
    @@ -0,0 +1,25 @@
    +  // During the update hooks the container is cleaned up to contain only
    +  // services that have their dependencies met. Core services are available.
    +  \Drupal::getContainer()->get('module_installer')->install(['new_dependency_test_with_service']);
    +
    +  \Drupal::state()->set(
    +    'new_dependency_test_update_8001.decorated_service',
    +    \Drupal::service('new_dependency_test.another_service')->greet()
    +  );
    +
    +  \Drupal::state()->set(
    +    'new_dependency_test_update_8001.decorated_service_custom_inner',
    +    \Drupal::service('new_dependency_test.another_service_two')->greet()
    +  );
    

    Could we invert the order and test the services that are not removed before installing the new module. The container doesn't get rebuilt until afterwards, but I think it would be easier to reason about. Alternatively we could also set a \Drupal::getContainer->has() to make sure in the update hook the other services still don't exist.

    Also we should maybe also add a comment that those services still exist. Ie they have their dependencies met and are not removed.

  2. +++ b/core/modules/system/tests/modules/new_dependency_test/new_dependency_test.services.yml
    @@ -0,0 +1,25 @@
    +  new_dependency_test.another_service:
    +    class: Drupal\new_dependency_test\Service
    +  new_dependency_test.another_service.decorated:
    +    class: Drupal\new_dependency_test\Service
    ...
    +  new_dependency_test.another_service_two:
    +    class: Drupal\new_dependency_test\Service
    +  new_dependency_test.another_service_two.decorated:
    +    class: Drupal\new_dependency_test\Service
    
    +++ b/core/modules/system/tests/modules/new_dependency_test/src/Service.php
    @@ -0,0 +1,18 @@
    +namespace Drupal\new_dependency_test;
    +
    +class Service {
    ...
    +  public function greet() {
    

    I would call the independent service differently, this is not enough for me to put it back to needs work, But another_service and also using the same greet method makes me think that they are more related than they are.
    I would go for new_dependency_test.independent for example and use a method called info.

Other than that I think it is good to RTBC.

alexpott’s picture

This patch addresses #104 and it also removes the usage of the word decorated when it is not to do with container service decoration as that is very confusing.

alexpott’s picture

In testing this patch with the Thunder 2 to Thunder 3 update path @chr.fritsch discovered that this causes problems for drush services - which are declared in drush.services.yml. This is because these are added to the container via a compiler pass. Which is exactly what we should be using here. There's a special stage for compiler passes that remove services - these are guaranteed to come at the end - so we can use that. This also means we can lose our special code for decorated services because we're now coming after \Symfony\Component\DependencyInjection\Compiler\DecoratorServicePass().

Also we can use Symfony's RepeatedPass code to keep the logic simpler.

alexpott’s picture

Note I think the latest patch fixes #78 too.

Here's how to test #106.

  1. Install Drupal 8.6.x
  2. Install config_split and config_filter (latest dev versions)
  3. Install Blazy 8.x-1.x
  4. Update code to Drupal 8.8.x and Blazy 8.x-2.x
  5. Run drush updatedb - it'll fail
  6. Apply patch
  7. It'll now work
bircher’s picture

Status: Needs review » Reviewed & tested by the community

Nice. I like the compiler pass better.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

Ho hum RepeatedPass is deprecated in Symfony 4.2 so let's not use that...

alexpott’s picture

Status: Needs work » Needs review
FileSize
11.13 KB
17.9 KB

Removed repeated pass and put more testing in place to ensure we triggered multiple runs through the while to remove services. Also added logging to the container compilation because we can.

alexpott’s picture

We can make further optimisations because of \Symfony\Component\DependencyInjection\Compiler\ResolveReferencesToAliasesPass which we come after.

bircher’s picture

Status: Needs review » Reviewed & tested by the community

Oh I didn't notice the Symfony 4 deprecation. Hmm I guess I will have to update my drupal dev setup to run with Symfony 4.
Great that you did!
The reorganisation in the test is much nicer now too.

gambry’s picture

Amazing work! This is one of the worst bug I always fear when upgrading contrib.

Two minor, which can be fixed on commit. So leaving RTBC.

+++ b/core/lib/Drupal/Core/Update/UpdateCompilerPass.php
@@ -0,0 +1,62 @@
+    // We only need to check aliases is we've removed a service because

Something weird with this sentence? is -> if?

+++ b/core/modules/system/tests/src/Functional/Update/UpdatePathNewDependencyTest.php
@@ -0,0 +1,91 @@
+    // dependency is still available while the ones that fail are not.
+
+    try {

Unneeded blank line?

alexpott’s picture

@gambry thanks for the review - yep those comments need work. I've re-read all the docs in \Drupal\Core\Update\UpdateCompilerPass and tried to improve it.

  • catch committed 621b626 on 8.8.x
    Issue #2863986 by bircher, alexpott, pfrenssen, claudiu.cristea, Adita,...
catch’s picture

Version: 8.8.x-dev » 8.7.x-dev
Status: Reviewed & tested by the community » Fixed

Committed/pushed to 8.8.x and cherry-picked to 8.7.x, thanks!

  • catch committed edc64bc on 8.7.x
    Issue #2863986 by bircher, alexpott, pfrenssen, claudiu.cristea, Adita,...
gambry’s picture

Don't you think this issue requires a mention in next release notes, or even a change record?
Although there are not API changes itself, we've been advocating for the usage of setter injection or ignoring missing dependencies, which now is not required anymore.

I believe quite few contrib and custom modules would love to know the problem has been fixed and they can now stop doing it?

slv_’s picture

Having had to find a workaround for this quite recently, a change record sounds like a good idea to me! Also, thanks to everybody who worked on this!

mxr576’s picture

#118 +1
#119 +1

bircher’s picture

Issue summary: View changes

Ok, for those who encountered this problem and didn't already follow this issue, I added a release not snippet and a change notice: https://www.drupal.org/node/3067480 someone can review it and publish it since this is already committed.

mpp’s picture

Fixed a typo and published the CR.

gambry’s picture

I was going to add a bit of formatting (see end of this comment), but I don't get what this paragraph means:

This allows installed modules to be refactored to depend on yet uninstalled modules and have the new modules services injected in the refactored services. The existing module must install the new module in an update hook because the normal service container will throw an exception when there are unmet dependencies.

What's "this"? UpdateKernel itself, or the changes?

I think our changes avoid a service refactoring, as long as the module install its dependencies on the hook_update()?

--- formatted CR ---
Summary
The UpdateKernel which is used by update.php to run the update hooks now removes services and aliases with unmet dependencies from its dependency injection container.

Before
Before this change module developers had to use setter injection or ignoring missing dependencies and guarding their services for those cases because update hooks could not be run due to the container throwing an exception.

After
The changes introduced with #2863986: Allow updating modules with new service dependencies allow installed modules to be refactored to depend on yet uninstalled modules and have the new modules services injected in the refactored services services to depend on yet uninstalled module services as long as the existing module install the new module in an update hook, and that's because the normal service container will throw an exception when there are unmet dependencies.

Please note those services with unmet dependencies will not be available during update hooks.

bircher’s picture

#123 Go ahead, change the node, it is already published anyway!

Yes "this" change allows us to refactor. I have no strong opinion about the change notice text in the first place because it was more of a wtf moment when it was not the way it is now after this patch. So I would be fine with just: "Finally you can add to and existing module a dependency on a new module without workarounds". But the structured explanation is much better of course.

Status: Fixed » Closed (fixed)

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

maximpodorov’s picture

FileSize
19.05 KB

Here is the patch for Drupal 8.6 based on the commit #117.

alexpott’s picture

We've got an advanced case over in #3085751: Setter injection arguments are not checked for unmet dependencies - fortunately this issue gives us all the plumbing to fix and test - so yay.

Sam152’s picture