Problem/Motivation

In Drupal 8, if a required argument is added to a service that is commonly used, the new version of the class won't get the parameter passed in until the container is updated.

This generally leads to errors like http://3v4l.org/J152X

Which looks like:

Additional uncaught exception thrown while handling exception.

Original

LogicException: A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break. in Drupal\Core\Render\Renderer->doRender() (line 485 of /8.x/core/lib/Drupal/Core/Render/Renderer.php).

Drupal\Core\Render\Renderer->doRender(Array, 1)
Drupal\Core\Render\Renderer->render(Array, 1)
Drupal\Core\Render\BareHtmlPageRenderer->renderBarePage(Array, 'Error', 'maintenance_page', Array)
Drupal\Core\DependencyInjection\Container\prod\Drupal_Core_Render_BareHtmlPageRenderer_Proxy->renderBarePage(Array, 'Error', 'maintenance_page')
_drupal_log_error(Array, 1)
_drupal_error_handler_real(4096, 'Argument 5 passed to Drupal\Core\Asset\AssetResolver::__construct() must implement interface Drupal\Core\Language\LanguageManagerInterface, none given, called in /8.x/sites/default/files/php/service_container/service_container_prod/759a4eaeb2792b2d134b2c734defeaf3080d18246ad32c33fe50b1e88a83aa47.php on line 1117 and defined', '/8.x/core/lib/Drupal/Core/Asset/AssetResolver.php', 79, Array)
_drupal_error_handler(4096, 'Argument 5 passed to Drupal\Core\Asset\AssetResolver::__construct() must implement interface Drupal\Core\Language\LanguageManagerInterface, none given, called in 8.x/sites/default/files/php/service_container/service_container_prod/759a4eaeb2792b2d134b2c734defeaf3080d18246ad32c33fe50b1e88a83aa47.php on line 1117 and defined', '8.x/core/lib/Drupal/Core/Asset/AssetResolver.php', 79, Array)
Drupal\Core\Asset\AssetResolver->__construct(Object, Object, Object, Object)
Drupal\Core\DependencyInjection\Container\prod\service_container_prod->getAsset_ResolverService()
Symfony\Component\DependencyInjection\Container->get('asset.resolver')
Drupal::service('asset.resolver')
template_preprocess_html(Array, 'html', Array)
Drupal\Core\Theme\ThemeManager->theme('html', Array)
Drupal\Core\Theme\ThemeManager->render('html', Array)
Drupal\Core\Render\Renderer->doRender(Array, )
Drupal\Core\Render\Renderer->render(Array)
Drupal\Core\Render\BareHtmlPageRenderer->renderBarePage(Array, 'Error', 'maintenance_page', Array)
Drupal\Core\DependencyInjection\Container\prod\Drupal_Core_Render_BareHtmlPageRenderer_Proxy->renderBarePage(Array, 'Error', 'maintenance_page')
Drupal\Core\EventSubscriber\DefaultExceptionSubscriber->onHtml(Object)
Drupal\Core\EventSubscriber\DefaultExceptionSubscriber->onException(Object, 'kernel.exception', Object)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch('kernel.exception', Object)
Symfony\Component\HttpKernel\HttpKernel->handleException(Object, Object, 1)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1)
Stack\StackedHttpKernel->handle(Object, 1, 1)
Drupal\Core\DrupalKernel->handle(Object)
Additional

LogicException: A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break. in Drupal\Core\Render\Renderer->doRender() (line 485 of 8.x/core/lib/Drupal/Core/Render/Renderer.php).

Drupal\Core\Render\Renderer->doRender(Array, 1)
Drupal\Core\Render\Renderer->render(Array, 1)
Drupal\Core\Render\BareHtmlPageRenderer->renderBarePage(Array, 'Error', 'maintenance_page', Array)
Drupal\Core\DependencyInjection\Container\prod\Drupal_Core_Render_BareHtmlPageRenderer_Proxy->renderBarePage(Array, 'Error', 'maintenance_page')
_drupal_log_error(Array, 1)
_drupal_error_handler_real(4096, 'Argument 5 passed to Drupal\Core\Asset\AssetResolver::__construct() must implement interface Drupal\Core\Language\LanguageManagerInterface, none given, called in /8.x/sites/default/files/php/service_container/service_container_prod/759a4eaeb2792b2d134b2c734defeaf3080d18246ad32c33fe50b1e88a83aa47.php on line 1117 and defined', '8.x/core/lib/Drupal/Core/Asset/AssetResolver.php', 79, Array)
_drupal_error_handler(4096, 'Argument 5 passed to Drupal\Core\Asset\AssetResolver::__construct() must implement interface Drupal\Core\Language\LanguageManagerInterface, none given, called in /8.x/sites/default/files/php/service_container/service_container_prod/759a4eaeb2792b2d134b2c734defeaf3080d18246ad32c33fe50b1e88a83aa47.php on line 1117 and defined', '8.x/core/lib/Drupal/Core/Asset/AssetResolver.php', 79, Array)
Drupal\Core\Asset\AssetResolver->__construct(Object, Object, Object, Object)
Drupal\Core\DependencyInjection\Container\prod\service_container_prod->getAsset_ResolverService()
Symfony\Component\DependencyInjection\Container->get('asset.resolver')
Drupal::service('asset.resolver')
template_preprocess_html(Array, 'html', Array)
Drupal\Core\Theme\ThemeManager->theme('html', Array)
Drupal\Core\Theme\ThemeManager->render('html', Array)
Drupal\Core\Render\Renderer->doRender(Array, )
Drupal\Core\Render\Renderer->render(Array)
Drupal\Core\Render\BareHtmlPageRenderer->renderBarePage(Array, 'Error', 'maintenance_page', Array)
Drupal\Core\DependencyInjection\Container\prod\Drupal_Core_Render_BareHtmlPageRenderer_Proxy->renderBarePage(Array, 'Error', 'maintenance_page')
_drupal_log_error(Array, 1)
_drupal_exception_handler(Object)
Drupal\Core\DrupalKernel->handleException(Object, Object, 1)
Drupal\Core\DrupalKernel->handle(Object)

This won't be fixed by running update.php - you have to run drush cr or rebuild.php first.

Also we just show a completely unthemed fatal error when this happens "Additional uncaught exception thrown while handling exception" - there is no hint of what goes on, and you can end up in the same situation due to bad code too.

In Drupal 7, certain core changes (changing the schema of the variables or node table, changing the contents of the system_info cache etc.) might result in fatal errors until update.php was run, however these were much rarer/exceptional situations.

Proposed resolution

There are several possible resolutions, we might want to do one or all of them:

1. Once the upgrade path is supported, completely disallow adding required arguments to service constructors. Default to NULL, remove typehints (turns a fatal to a warning), use setter injection, or use \Drupal:service(). This circumvents the problem at the cost of adding technical debt and would be a change people would need to get used to. Also it doesn't help when contrib change their services.

2. Base the container hash on the VERSION constant, so that both minor and patch versions immediately invalidate the container as soon as code is deployed. This immediately helps core (and contrib if updates are done at the same time as a core update). It won't help contrib updates done in isolation though.

3. Since the missing type hint is a catchable fatal error (or a TypeError exception in PHP 7), have a global try/catch for errors of this type, and when we hit one, invalidate the container, show a 503 and either automatically redirect to self (dawehner recommended doing so with a query parameter to prevent recursion) or show a link to reload. This would work for core, contrib, and people running development versions of core, but might be a bit messy.

4. Add an extra hash bit (in addition to VERSION) configurable from settings.php to allow people to do the equivalent of #2 manually for deployments.

Remaining tasks

User interface changes

API changes

Comments

catch’s picture

Issue summary: View changes
dawehner’s picture

As said in IRC, the approach in 2) does not solve the contrib / custom space, where people are certainly much easier run into the issue,
because compared to core contributors, they aren't aware of that problem.

Solution 1) is also problematic as it makes it harder to explain to people ... I think the only proper approach will be 3) ...

@damiankloip suggested to redirect to rebuild.sh

catch’s picture

Issue summary: View changes
catch’s picture

I think #3 is good as a fallback, but I'm concerned about relying 100% on a successful rebuild triggered via our exception handling.

I just added #4 which would let people set their own additional hash key in settings.php for deployments - that would have the same behaviour as #2 but mean that individual sites would be able to run without relying on #3 (if they're careful).

catch’s picture

Priority: Major » Critical

Since #1 is IMO our only 'workaround', and could result in a massive slowdown in critical issue fixing, I'm going to bump this to critical.

I think we can do option #2 very quickly to support the beta to beta upgrade path, and option #3 would then be either major or a non-beta-to-beta-upgrade-path-blocking critical that could happen separately.

dawehner’s picture

Status: Active » Needs review
StatusFileSize
new563 bytes

There we go.

catch’s picture

Yep, that should do it.

So this partially helps with beta to beta upgrade path including contrib too. You just have to only upgrade contrib when you also upgrade core. It's not acceptable for 8.0.0 but that's an OK workaround for someone following betas that doesn't want to be messing around with manually triggering container rebuilds.

Status: Needs review » Needs work

The last submitted patch, 6: 2507509-6.patch, failed testing.

catch’s picture

Status: Needs work » Needs review
StatusFileSize
new1.16 KB

No dots in class names.

catch’s picture

StatusFileSize
new577 bytes

Less cruft in patches....

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

Status: Needs review » Needs work

The last submitted patch, 10: 2507509-9.patch, failed testing.

catch’s picture

Status: Needs work » Needs review
StatusFileSize
new577 bytes

One line....

catch’s picture

StatusFileSize
new578 bytes

That could amusingly result in function names with '-' on 32bit systems for particular hashes. See http://php.net/crc32

Should possibly give up and use hash('sha512') or whatever but, sticking with cr32 for now since this will impact page cache performance.

dawehner’s picture

Ha, congrats to that cooperative failing :)

The last submitted patch, 13: third_times_the_charm.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 14: third_times_the_charm.patch, failed testing.

catch’s picture

Status: Needs work » Needs review
StatusFileSize
new579 bytes
chx’s picture

I am wondering whether the ExtensionDiscovery could supply us with the module versions and if so then could we add the modulename-versionnumber as cache tags to #2497243: Replace Symfony container with a Drupal one, stored in cache solving this problem here.

dawehner’s picture

Mh, what happens then if you are using git to store your modules?

catch’s picture

Issue summary: View changes
chx’s picture

dawehner’s picture

@chx
Sure, but we are talking about a low level feature of Drupal vs. update module in Drupal 7.

chx’s picture

It was just an idea. I do not have good ideas any more.

jibran’s picture

Is it worth adding tests?

dawehner’s picture

Mh that is quite hard to test to be fair.
A quick idea I had was to move the version appending into index.php, which then would allow us to at least kernel test this.

catch’s picture

Should we also in this issue add the customizable prefix in $settings here? That would at least allow people to manually update settings.php when they do deployments, so it gives a workaround for the contrib/custom module situation.

I think we should open a follow-up to discuss chx's idea more. Most sites I work on tend to download tarballs, extract, add to git rather than using actual git checkouts of modules. If you are using git checkouts of modules, then you're more capable of manually updating settings.php (or having a build step do it), or running rebuild.php on deploy etc.

I've opened a separate issue for the failure in our exception handling at #2509898: Additional uncaught exception thrown while handling exception after service changes

dawehner’s picture

Should we also in this issue add the customizable prefix in $settings here? That would at least allow people to manually update settings.php when they do deployments, so it gives a workaround for the contrib/custom module situation.

Sounds like a good idea, its a simple way to achieve that.

catch’s picture

StatusFileSize
new1.5 KB

Here's a patch for that.

dawehner’s picture

+++ b/sites/default/default.settings.php
@@ -287,6 +287,18 @@
+ * Deployment identifier.
+ *
+ * Drupal's dependency injection container will be automatically invalidated and
+ * rebuilt when the Drupal core version changes. When updating contributed or
+ * custom code that changes the container, changing this identifier will also
+ * allow the container to be invalidated as soon as code is deployed. This ID
+ * must only use characters valid in PHP function names as defined by
+ * http://php.net/manual/en/functions.user-defined.php
+ */
+$settings['deployment_identifier'] = '';

I'm curious whether we should move \Drupal::version() into the deployment identifier by default, so people get an idea how it varies.

catch’s picture

If we do that, we'll have to hash the identifier in the same way it's done in the patch where we already use VERSION.

That would stop people breaking their own sites by putting characters invalid for function names though.

dawehner’s picture

Doesn't sound like a totally bad idea, it would also make it testable, as you can manipulate settings.php easily in your test.

fabianx’s picture

Status: Needs review » Needs work

Yeah, lets put it in the hash at least.

catch’s picture

Status: Needs work » Needs review
StatusFileSize
new1.16 KB
new1.51 KB

This puts the setting in the hash and uses \Drupal::VERSION as the default in default.settings.php.

It means we hash Drupal::VERSION . Drupal::VERSION by default but I don't think that's a problem.

fabianx’s picture

Status: Needs review » Needs work
Issue tags: +D8 Accelerate
+++ b/sites/default/default.settings.php
@@ -287,6 +287,18 @@
+ * allow the container to be invalidated as soon as code is deployed. This ID
+ * must only use characters valid in PHP function names as defined by
+ * http://php.net/manual/en/functions.user-defined.php

This can be removed now.

One thing, then RTBC.

dawehner’s picture

Does anyone disagree that we should have some basic test coverage for this?

Steps:

  • call drupalGet()
  • set state, and use that state in the container rebuild process
  • change settings.php, we have even an api function for that
  • rebuild the container
  • Call another drupalGet()
alexpott’s picture

Issue tags: +Needs tests

I agree with @dawehner a test would be good.

+++ b/sites/default/default.settings.php
@@ -287,6 +287,18 @@
+$settings['deployment_identifier'] = \Drupal::VERSION;

Let's comment this out. I don't think it always needs to be set.

dawehner’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests
StatusFileSize
new6.21 KB
new5.2 KB

Wow, $this->writeSettings is interesting to use.

Here is the test coverage.

fabianx’s picture

Status: Needs review » Reviewed & tested by the community

RTBC, looks great!

wim leers’s picture

Elegant :)

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed 2e66338 and pushed to 8.0.x. Thanks!

diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php
index 3516f77..330cc56 100644
--- a/sites/default/default.settings.php
+++ b/sites/default/default.settings.php
@@ -292,9 +292,7 @@
  * Drupal's dependency injection container will be automatically invalidated and
  * rebuilt when the Drupal core version changes. When updating contributed or
  * custom code that changes the container, changing this identifier will also
- * allow the container to be invalidated as soon as code is deployed. This ID
- * must only use characters valid in PHP function names as defined by
- * http://php.net/manual/en/functions.user-defined.php
+ * allow the container to be invalidated as soon as code is deployed.
  */
 # $settings['deployment_identifier'] = \Drupal::VERSION;
 

As @Fabianx noted the limitation to use only valid characters for PHP function names is no longer necessary. Removed on commit.

  • alexpott committed 2e66338 on 8.0.x
    Issue #2507509 by catch, dawehner: Service changes should not result in...
Jaesin’s picture

+++ b/sites/default/default.settings.php
@@ -287,6 +287,18 @@
 /**
+ * Deployment identifier.
+ *
+ * Drupal's dependency injection container will be automatically invalidated and
+ * rebuilt when the Drupal core version changes. When updating contributed or
+ * custom code that changes the container, changing this identifier will also
+ * allow the container to be invalidated as soon as code is deployed. This ID
+ * must only use characters valid in PHP function names as defined by
+ * http://php.net/manual/en/functions.user-defined.php
+ */
+# $settings['deployment_identifier'] = \Drupal::VERSION;

This is super handy. Nice!

Status: Fixed » Closed (fixed)

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