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

Command icon 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

penyaskito created an issue. See original summary.

phenaproxima’s picture

Priority: Normal » Critical
Issue tags: +Drupal CMS dependency critical

I'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() in canvas_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\SessionNotFoundException being 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.

phenaproxima’s picture

To 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.

penyaskito’s picture

penyaskito’s picture

Entire stack trace:

NOTICE: PHP message: Symfony\Component\HttpFoundation\Exception\SessionNotFoundException: There is currently no session available. in /var/www/html/vendor/symfony/http-foundation/RequestStack.php on line 115 #0 /var/www/html/web/core/lib/Drupal/Core/TempStore/SharedTempStoreFactory.php(93): Symfony\Component\HttpFoundation\RequestStack->getSession()
#1 /var/www/html/web/core/modules/package_manager/src/SandboxManagerBase.php(186): Drupal\Core\TempStore\SharedTempStoreFactory->get()
#2 /var/www/html/web/modules/contrib/automatic_updates/src/UpdateSandboxManager.php(54): Drupal\package_manager\SandboxManagerBase->__construct()
#3 [internal function]: Drupal\automatic_updates\UpdateSandboxManager->__construct()
#4 /var/www/html/vendor/symfony/dependency-injection/ContainerBuilder.php(1197): ReflectionClass->newInstanceArgs()
#5 /var/www/html/vendor/symfony/dependency-injection/ContainerBuilder.php(632): Symfony\Component\DependencyInjection\ContainerBuilder->createService()
#6 /var/www/html/vendor/symfony/de...
2026/03/21 05:35:11 [error] 2095#2095: *161 FastCGI sent in stderr: "PHP message: Symfony\Component\HttpFoundation\Exception\SessionNotFoundException: There is currently no session available. in /var/www/html/vendor/symfony/http-foundation/RequestStack.php on line 115 #0 /var/www/html/web/core/lib/Drupal/Core/TempStore/SharedTempStoreFactory.php(93): Symfony\Component\HttpFoundation\RequestStack->getSession()
#1 /var/www/html/web/core/modules/package_manager/src/SandboxManagerBase.php(186): Drupal\Core\TempStore\SharedTempStoreFactory->get()
#2 /var/www/html/web/modules/contrib/automatic_updates/src/UpdateSandboxManager.php(54): Drupal\package_manager\SandboxManagerBase->__construct()
#3 [internal function]: Drupal\automatic_updates\UpdateSandboxManager->__construct()
#4 /var/www/html/vendor/symfony/dependency-injection/ContainerBuilder.php(1197): ReflectionClass->newInstanceArgs()
#5 /var/www/html/vendor/symfony/dependency-injection/ContainerBuilder.php(632): Symfony\Component\DependencyInjection\ContainerBuilder->createService()
#6 /var/www/html/vendor/symfony/de" while reading response header from upstream, client: 172.18.0.6, server: , request: "POST /core/install.php?profile=drupal_cms_installer&langcode=en&name=1&template=1&id=1&op=do_nojs&op=do&_format=json HTTP/1.1", upstream: "fastcgi://unix:/run/php-fpm.sock:", host: "drupal-cms-dev.ddev.site", referrer: "https://drupal-cms-dev.ddev.site/core/install.php?profile=drupal_cms_installer&langcode=en&name=1&template=1&id=1&op=start"
phenaproxima’s picture

If we can get rid of the drupal_flush_all_caches call, we will be okay and could release another hotfix.

penyaskito’s picture

I'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_caches is 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_installed implementations of other modules.
In others, it complained about trying to get the synthetic kernel service (which ComponentSourceManager gets injected).

At this point I better try to get themes allowed as module dependencies in core.

wim leers’s picture

Issue tags: +Needs tests

How can we ensure we never need to be surprised by this again?

  1. Why did Canvas’ explicit recipe tests not hit this problem? Can we add the necessary test coverage?
  2. If we can’t: can we add a CI job with a script that would reproduce this? Sounds like this needs Automatic Updates to do something after Canvas is installed in the same request? 😦

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.

phenaproxima’s picture

No, 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.

phenaproxima’s picture

!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:

Symfony\Component\DependencyInjection\Exception\RuntimeException: You have requested a synthetic service ("kernel"). The DIC does not know how to construct this service.

/Users/phen/Sites/canvas-dev/vendor/symfony/dependency-injection/ContainerBuilder.php:1099
/Users/phen/Sites/canvas-dev/vendor/symfony/dependency-injection/ContainerBuilder.php:632
/Users/phen/Sites/canvas-dev/vendor/symfony/dependency-injection/ContainerBuilder.php:1315
/Users/phen/Sites/canvas-dev/vendor/symfony/dependency-injection/ContainerBuilder.php:1267
/Users/phen/Sites/canvas-dev/vendor/symfony/dependency-injection/ContainerBuilder.php:1167
/Users/phen/Sites/canvas-dev/vendor/symfony/dependency-injection/ContainerBuilder.php:632
/Users/phen/Sites/canvas-dev/vendor/symfony/dependency-injection/ContainerBuilder.php:1315
/Users/phen/Sites/canvas-dev/vendor/symfony/dependency-injection/ContainerBuilder.php:1267
/Users/phen/Sites/canvas-dev/vendor/symfony/dependency-injection/ContainerBuilder.php:1167
/Users/phen/Sites/canvas-dev/vendor/symfony/dependency-injection/ContainerBuilder.php:632
/Users/phen/Sites/canvas-dev/vendor/symfony/dependency-injection/ContainerBuilder.php:577
/Users/phen/Sites/canvas-dev/web/core/lib/Drupal/Core/DependencyInjection/ClassResolver.php:28
/Users/phen/Sites/canvas-dev/web/core/lib/Drupal/Core/Utility/CallableResolver.php:100
/Users/phen/Sites/canvas-dev/web/core/lib/Drupal/Core/Extension/ModuleHandler.php:742
/Users/phen/Sites/canvas-dev/web/core/lib/Drupal/Core/Extension/ModuleHandler.php:338
/Users/phen/Sites/canvas-dev/web/core/lib/Drupal/Core/Extension/ModuleHandler.php:388
/Users/phen/Sites/canvas-dev/web/core/lib/Drupal/Core/Extension/ModuleInstaller.php:257
/Users/phen/Sites/canvas-dev/web/core/lib/Drupal/Core/ProxyClass/Extension/ModuleInstaller.php:83
/Users/phen/Sites/canvas-dev/web/core/lib/Drupal/Core/Recipe/RecipeRunner.php:275
/Users/phen/Sites/canvas-dev/web/core/includes/batch.inc:300
/Users/phen/Sites/canvas-dev/web/core/includes/form.inc:834
/Users/phen/Sites/canvas-dev/web/core/includes/install.core.inc:647
/Users/phen/Sites/canvas-dev/web/core/includes/install.core.inc:565
/Users/phen/Sites/canvas-dev/web/core/includes/install.core.inc:122
/Users/phen/Sites/canvas-dev/web/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php:326
/Users/phen/Sites/canvas-dev/web/core/tests/Drupal/Tests/BrowserTestBase.php:554
/Users/phen/Sites/canvas-dev/web/core/tests/Drupal/Tests/BrowserTestBase.php:349

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.

phenaproxima’s picture

Playing with this a bit more and I discovered that removing the drupal_flush_all_caches call 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.

penyaskito’s picture

#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.

fago made their first commit to this issue’s fork.

fago’s picture

The 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 44

Update: unrelated issue, somehow this is triggered by canvas now. nevermind. this got fixed in #3517882: The namespace of EmbedCKEditor5PluginBase does not respect PSR4.

phenaproxima’s picture

Still working on this.

The line that causes the exception in \Drupal\Tests\canvas\Functional\InstallerTest is:

\Drupal::service(ThemeInstallerInterface::class)->install(['canvas_stark']);

...in canvas_install.

wim leers’s picture

Assigned: Unassigned » phenaproxima

@phenaproxima: thanks — this seems quite important to resolve indeed 🙏 Reflecting in issue metadata.

phenaproxima’s picture

Discussed 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_stark when Canvas is? We could do this in hook_modules_installed.

Except for one PHPUnit test, all passed. Yes, even the end-to-end ones. Holy 💩!

This lets us remove the entire canvas_install function. I think we should take it one step further, and use PHPCS to forbid all calls to drupal_flush_all_caches(). It's a bad function that should never be used.

phenaproxima’s picture

All tests should now be passing.

I ended up rewriting \Drupal\Tests\canvas\Functional\UninstallModulePageTest because 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's InstallerTestBase, 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.

phenaproxima’s picture

Title: Remove `drupal_flush_all_caches` call from canvas_install() and use `container_rebuild_required: true` in info.yml instead. » Remove canvas_install() entirely and trigger the installation of canvas_stark unconditionally when Canvas itself is installed
Assigned: phenaproxima » Unassigned
Status: Active » Needs review
phenaproxima’s picture

Title: Remove canvas_install() entirely and trigger the installation of canvas_stark unconditionally when Canvas itself is installed » Remove canvas_install() entirely and trigger the installation of canvas_stark unconditionally when Canvas itself is installed; forbid the use of `drupal_flush_all_caches` in Canvas's code base
Issue tags: +Needs issue summary update

I'll need to rewrite the issue summary for this one.

phenaproxima changed the visibility of the branch 3580247-remove-drupalflushallcaches-call to hidden.

phenaproxima changed the visibility of the branch 3580247-container-rebuild to hidden.

phenaproxima changed the visibility of the branch 3580247-hotfix to hidden.

phenaproxima’s picture

Issue summary: View changes
Issue tags: -Needs tests, -Needs issue summary update
penyaskito’s picture

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

I 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.

phenaproxima’s picture

Assigned: phenaproxima » penyaskito
Status: Needs work » Needs review

Back to you.

penyaskito’s picture

Assigned: penyaskito » Unassigned
Status: Needs review » Reviewed & tested by the community

RTBC

  • phenaproxima committed 45e0f8c6 on 1.x
    fix: #3580247 Remove canvas_install() entirely and trigger the...
phenaproxima’s picture

Status: Reviewed & tested by the community » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

wim leers’s picture

🤩 Fantastic to see this improved!

Status: Fixed » Closed (fixed)

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