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
| Comment | File | Size | Author |
|---|---|---|---|
| #38 | interdiff.txt | 5.2 KB | dawehner |
| #38 | 2507509-38.patch | 6.21 KB | dawehner |
| #34 | 2507509.patch | 1.51 KB | catch |
| #34 | interdiff.txt | 1.16 KB | catch |
| #29 | 2507509.patch | 1.5 KB | catch |
Comments
Comment #1
catchComment #2
dawehnerAs 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
Comment #3
catchComment #4
catchI 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).
Comment #5
catchSince #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.
Comment #6
dawehnerThere we go.
Comment #7
catchYep, 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.
Comment #9
catchNo dots in class names.
Comment #10
catchLess cruft in patches....
Comment #13
catchOne line....
Comment #14
catchThat 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.
Comment #15
dawehnerHa, congrats to that cooperative failing :)
Comment #18
catchComment #19
chx commentedI 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.
Comment #20
dawehnerMh, what happens then if you are using git to store your modules?
Comment #21
catchComment #22
chx commented#20 https://www.drupal.org/project/git_deploy ?
Comment #23
dawehner@chx
Sure, but we are talking about a low level feature of Drupal vs. update module in Drupal 7.
Comment #24
chx commentedIt was just an idea. I do not have good ideas any more.
Comment #25
jibranIs it worth adding tests?
Comment #26
dawehnerMh 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.
Comment #27
catchShould 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
Comment #28
dawehnerSounds like a good idea, its a simple way to achieve that.
Comment #29
catchHere's a patch for that.
Comment #30
dawehnerI'm curious whether we should move \Drupal::version() into the deployment identifier by default, so people get an idea how it varies.
Comment #31
catchIf 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.
Comment #32
dawehnerDoesn't sound like a totally bad idea, it would also make it testable, as you can manipulate settings.php easily in your test.
Comment #33
fabianx commentedYeah, lets put it in the hash at least.
Comment #34
catchThis 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.
Comment #35
fabianx commentedThis can be removed now.
One thing, then RTBC.
Comment #36
dawehnerDoes anyone disagree that we should have some basic test coverage for this?
Steps:
drupalGet()drupalGet()Comment #37
alexpottI agree with @dawehner a test would be good.
Let's comment this out. I don't think it always needs to be set.
Comment #38
dawehnerWow,
$this->writeSettingsis interesting to use.Here is the test coverage.
Comment #39
fabianx commentedRTBC, looks great!
Comment #40
wim leersElegant :)
Comment #41
alexpottCommitted 2e66338 and pushed to 8.0.x. Thanks!
As @Fabianx noted the limitation to use only valid characters for PHP function names is no longer necessary. Removed on commit.
Comment #43
Jaesin commentedThis is super handy. Nice!