Overview
We have a call to `drupal_flush_all_caches` in `canvas_install()`.
That's considered a bad practice, and shouldn't be needed.
Proposed resolution
Generally speaking, we should not need canvas_install() at all. Its only purpose is to install canvas_stark, but this doesn't work properly because it causes a container rebuild while the container is being rebuilt due to the module being installed. (Hey bro, I heard you like container rebuilds...)
We can do better than this. canvas_stark is needed for Canvas to operate, so it should be installed unconditionally, regardless of config sync, when Canvas itself has been installed (the past tense there is the key). We can do that in a hook_modules_installed implementation.
Meanwhile, we can remove the drupal_flush_all_caches call; tests pass without it, so it is apparently not necessary. UninstallModulePageTest needs to be rewritten because it's outdated and, to be honest, nonsensical.
To keep people away from the destructive sledgehammer that is drupal_flush_all_caches, let's also add a PHPCS rule which outright forbids calling it.
User interface changes
None.
Issue fork canvas-3580247
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
phenaproximaI'm sorry to say that this broke Drupal CMS completely. It is not installable, at all, with Canvas 1.3.0.
The problem is the call to
drupal_flush_all_caches()incanvas_install(). This causes the request stack to be recreated without the current request, which means the session is unavailable -- and that, in turn leads to a\Symfony\Component\HttpFoundation\Exception\SessionNotFoundExceptionbeing thrown by\Symfony\Component\HttpFoundation\RequestStack::getSession()if anything even inadvertently calls it -- in our case, it's because Automatic Updates tries to get the shared tempstore, which needs to examine the session during initialization.But, I hear you saying, the
drupal_flush_all_caches()call is in 1.2.0 also, and that was working!Well...no it wasn't. It was in fact being masked by an early return if $is_syncing was TRUE, which it is while applying recipes (which is 95% of what the Drupal CMS installer does). That early return was removed in 1.3.0.
Since this is the eve of Drupal CMS 2.1.0's release, the timing is extremely bad.
We cannot work around this by pinning to Canvas 1.2.0, because the pinned dependency will end up in the top-level composer.json (due to recipe unpacking) and make Canvas non-updateable unless you know to edit composer.json. This is why Drupal CMS never, ever pins dependencies under any circumstances.
There is only one course of action: the $is_syncing early return has to be restored, and then Canvas 1.3.1 must come out as a hotfix release. This is a Drupal CMS release blocker.
Comment #4
phenaproximaTo be clear on the urgency of this: I think this is also breaking the current stable release!
In other words, Drupal CMS is not installable, in any current stable version, until we've hotfixed this.
Comment #6
penyaskitoNow this is tied to #3562354: canvas_stark not installed when canvas installed from a recipe, where this should have been fixed instead.
Comment #7
penyaskitoEntire stack trace:
Comment #8
phenaproximaIf we can get rid of the drupal_flush_all_caches call, we will be okay and could release another hotfix.
Comment #9
penyaskitoI'm not 100% sure the problem that happened with Drupal CMS is because of
drupal_flush_all_caches. It might be installing the theme itself.drupal_full_all_cachesis a horrible thing to do, but supported. It even has explicit test coverage in\Drupal\FunctionalTests\Installer\DrupalFlushAllCachesInInstallerTest. The problem somehow is when this (or the theme installation) happens in the context of applying a recipe.I've tried quite some things: in some cases it failed on
hook_module_installedimplementations of other modules.In others, it complained about trying to get the synthetic kernel service (which
ComponentSourceManagergets injected).At this point I better try to get themes allowed as module dependencies in core.
Comment #10
wim leersHow can we ensure we never need to be surprised by this again?
Drupal’s many abstraction layers, each with their own bugs/edge case/caveats and most with poor DX and edge case handling is what is led us to this place.
Comment #11
phenaproximaNo, no, no. There is NOTHING Automatic Updates could have done to prevent this, and I can walk you through what happened, over Zoom, if you don’t believe me.
The only reason Automatic Updates is in the blast radius is because it has a hook service which injects another service that, in its dependency tree, requires the shared tempstore factory. The shared tempstore factory is asking for the request, and the container doesn't have it due to the reset Canvas is doing.
I will try to give you a test case for this. I would think that it should be reproducible.
Comment #13
phenaproxima!780 supplies a test which tries to install Drupal using a simple recipe that installs only two modules: Canvas, and a stub module with a no-op hook service that injects the shared temp store. That is, overall, essentially what the Drupal CMS installer is doing: installing Drupal from a recipe.
When you remove the $is_syncing early return from canvas_install, the test dies with a very, very similar exception:
I think that this is a different manifestation of the same bug: when Canvas blows away the container, some crucial services stop working because of the indiscriminate nature of the reset. The test passes if you restore the $is_syncing check, which would seem to confirm that it is masking the bug.
When I paused this test during debugging and examined the call stack, I discovered that this particular exception path is being caused by
\Drupal\canvas\Hook\ComponentSourceHooks::modulesInstalled(), because the service is trying to construct\Drupal\canvas\ComponentSource\ComponentSourceManager, which has the kernel as a dependency.The fact that
\Drupal\canvas\Hook\ComponentSourceHooks::modulesInstalled()returns early if config is syncing is of no help (Automatic Updates has this guard rail too), because the problem is happening before the hook is even invoked -- it's crapping out while the hook service is still being constructed by the container.So it's the same basic cause: a service innocently depends on a service that no longer exists, or has changed state in a breaking way, because the container it thought it had, is now gone.
Comment #14
phenaproximaPlaying with this a bit more and I discovered that removing the
drupal_flush_all_cachescall doesn't resolve the exception the test is turning up.This still shows that installing Canvas via a recipe at install time breaks without the $is_syncing guard, but why seems a little more ambiguous.
Comment #15
penyaskito#13: Great job and thanks for putting that together, that's exactly where I reached by debugging the issue. And confirming #14. Which is what confuses the heck out of me and the reason I wrote #9.
Comment #17
fagoThe current band-aid still causes issues for me when installing via recipes. On a site that has entity_embed installed, applying a recipe to install canvas with its config causes this error for me:
PHP Fatal error: Cannot declare class Drupal\entity_embed\Plugin\CKEditor5Plugin\EmbedCKEditor5PluginBase, because the name is already in use in /app/web/modules/contrib/embed/src/Plugin/CKEditor5Plugin/EmbedCKEditor5PluginBase.php on line 44Update: unrelated issue, somehow this is triggered by canvas now. nevermind. this got fixed in #3517882: The namespace of EmbedCKEditor5PluginBase does not respect PSR4.
Comment #18
phenaproximaStill working on this.
The line that causes the exception in
\Drupal\Tests\canvas\Functional\InstallerTestis:...in
canvas_install.Comment #19
wim leers@phenaproxima: thanks — this seems quite important to resolve indeed 🙏 Reflecting in issue metadata.
Comment #20
phenaproximaDiscussed with @bnjmnm and my feeling is that Claude was correctly identifying "rebuilding the container while rebuilding the container" as the source of a lot of fragility. I implemented an experiement: what if we unconditionally install
canvas_starkwhen Canvas is? We could do this inhook_modules_installed.Except for one PHPUnit test, all passed. Yes, even the end-to-end ones. Holy 💩!
This lets us remove the entire
canvas_installfunction. I think we should take it one step further, and use PHPCS to forbid all calls todrupal_flush_all_caches(). It's a bad function that should never be used.Comment #21
phenaproximaAll tests should now be passing.
I ended up rewriting
\Drupal\Tests\canvas\Functional\UninstallModulePageTestbecause it is extremely outdated, harkening back to when Canvas was called Experience Builder...and it makes, to be very frank, zero sense as written. It's extending core'sInstallerTestBase, which is for testing the UI installer, not the module uninstall page. It is also using outdated configuration which no longer applies. It should never have passed at all, but somehow it has been, essentially by coincidence. I looked at the issue which introduced it (#3464830: FieldTypeUninstallValidator: TypeError: str_starts_with(): Argument #1 ($haystack) must be of type string, null given in str_starts_with()) and I think I'm keeping the intention of the test, which was to confirm that /admin/modules/uninstall can be visited without a fatal error.Comment #22
phenaproximaComment #23
phenaproximaI'll need to rewrite the issue summary for this one.
Comment #27
phenaproximaComment #28
penyaskitoI hate we might be already altering the config that the user might be importing if they didn't have the theme enabled. It's unexpected, very bad DX. But this is a hard-requirement, modules cannot depend on themes (see #474684: Allow themes to declare dependencies on modules for a lot of background on why we didn't add it both ways).
But we have no better choice.
Let's convert the phpcs check into a phpstan rule. See #3584511: PHPStan: convert PHPCS `Canvas.Tests.KernelTestBase.RequireAssertEntityIsValid` rule to a PHPStan rule for benefits. Otherwise this LGTM.
Comment #29
phenaproximaBack to you.
Comment #30
penyaskitoRTBC
Comment #32
phenaproximaComment #34
wim leers🤩 Fantastic to see this improved!