Problem/Motivation

Testing installing a module via project browser, I noticed 10 separate calls to ModuleInstaller::install(). The project browser request to install the recipe required a total of over 160mb of memory and took 14 seconds.

This is from RecipeRunner::processInstall() which installs modules one by one.

It means it can't take advantage of #3416522: Add the ability to install multiple modules and only do a single container rebuild to ModuleInstaller, at all, which by itself will be a significant improvement from 11.2 onwards, but ModuleInstaller::install() in 11.1 already has some optimisations when multiple modules are installed at once. e.g. the router is rebuilt 10 times in that request, taking over 2.5 seconds, instead of once.

Proposed resolution

Use the ability to install multiple modules with a single container rebuild in the recipe system.

A new RecipeRunner::installModules() method replaces the single-module RecipeRunner::installModule() (which is now deprecated). Both processInstall() and toBatchOperationsInstall() now collect all modules to install and pass them to installModules() in chunks, controlled by the core.multi_module_install_batch_size setting (default: 20). This allows ModuleInstaller::install() to be called once per chunk instead of once per module, avoiding redundant router rebuilds and container rebuilds.

To do this the MR adds a new RecipeMultipleModulesConfigStorage class. When installing modules in bulk, the ConfigInstaller needs access to the config/install directories of all modules being installed at the same time. RecipeMultipleModulesConfigStorage is a read-only StorageInterface implementation that combines multiple modules' config directories into a single storage. It ensures configuration isolation: each config object is only read from the module that owns it, determined by matching the config name prefix (the part before the first .) to the module name. For example, system.site is read from the system module's storage, never from another module — even if another module ships a system.site.yml in its config/install directory. This prevents one module from overriding another module's configuration during a multi-module install.

Remaining tasks

User interface changes

None

Introduced terminology

None

API changes

  • new RecipeMultipleModulesConfigStorage class
  • RecipeRunner::installModule() is deprecated
  • RecipeRunner::installModules() is added

Note that the Recipe API is all still marked as @internal and final.

Data model changes

None

Release notes snippet

Issue fork drupal-3498026

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

catch created an issue. See original summary.

catch’s picture

Issue summary: View changes
alexpott’s picture

Note we need to think about \Drupal\Core\Recipe\RecipeRunner::toBatchOperationsInstall() too

It's likely this change will result in quite a bit of refactoring.

alexpott’s picture

The tricky bit here will be

    // Disable configuration entity install but use the config directory from
    // the module.
    \Drupal::service('config.installer')->setSyncing(TRUE);
    $default_install_path = \Drupal::service('extension.list.module')->get($module)->getPath() . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY;
    // Allow the recipe to override simple configuration from the module.
    $storage = new RecipeOverrideConfigStorage(
      $recipeConfigStorage,
      new FileStorage($default_install_path, StorageInterface::DEFAULT_COLLECTION)
    );

@phenaproxima suggested wrapping all the config from all the modules together. This might work because modules cannot have conflicting config - we have the PreExistingConfiguration exception that handles that. I'm concerned that it might not work because that when you install the first module it'd discover all the configuration from the modules that are yet to be installed. Not sure how that's going to work out.

mandclu’s picture

+1 on the need for this. I can install the Event Platform project (which has loads of nested dependencies) in less than 20 seconds via the Drupal UI or drush. If I implement a recipe that installs this project (and does nothing else, not even forcing the configuration to load) then the recipe runner tries for several minutes and then inevitably fails.

mandclu’s picture

I will also add that this seems to severely impact the intended reusability of the recipes within Drupal CMS. I can include one such recipe in my own recipe (tested successfully with drupal_cms_admin_ui) but my recipe fails to apply if I add another (tested by adding drupal_cms_anti_spam, which only adds another 5 dependencies, based on the command line output).

thejimbirch’s picture

Issue tags: +Recipes initiative
catch’s picture

Would be good to get recipe/Drupal CMS installer performance up to the same level as module installs everywhere else, so tagging with 11.3.0 release priority/highlights.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.

alexpott’s picture

phenaproxima’s picture

Tested this locally by installing Byte. The computer in question is a pretty snappy M1 Pro. I was running Drupal CMS 2.x-dev HEAD, under DDEV.

I timed it using my phone's stopwatch, and it clocked in at 26.08 seconds with the patch applied.

Then I removed the patch and tried again: 45.18 seconds.

I'd call that a major improvement!

alexpott’s picture

Things to do at this point:

  • Add test coverage for the stream wrappers / work out if it is the best implementation (also check for existing issues)
  • Add test coverage of the new \Drupal\Core\Recipe\RecipeMultipleModulesConfigStorage config storage implementation
catch’s picture

alexpott’s picture

@catch thanks - I've moved my streamwrapper to a separate MR attached to that issue as the fix I did here works for that too... so we should get that in first.

alexpott’s picture

Merged main... should be back to green.

alexpott’s picture

So the one tricky thing with this MR is that it allows side effects during module install if a module contains simple configuration from another module. Because it reads all the module's config directories at the same time. Module's should never provide simple configuration for another module but things happen. We could potentially use this fact to limit to only reading from storages that match the config prefix.... so for example if you're reading system.site it will only read the system module's config directory.

I think this would be much safer and more predictable.

alexpott’s picture

I've addressed #18. The updated RecipeMultipleModulesConfigStorage will only read config starting with a module's name form that module's config folder.

I need to improve the documentation in the class as why this is important and why it works. It's all about \Drupal\Core\Config\ConfigInstaller::installDefaultConfig() lines 127-132.

alexpott’s picture

I've added documentation and improved the code and test coverage. This is now definitely in the needs review stage.

alexpott’s picture

Issue summary: View changes

Updated the issue summary with the solution from the MR.

alexpott’s picture

Issue summary: View changes
clayfreeman’s picture

Some sites have very strict memory requirements, so whatever approach is taken with respect to installing multiple modules per request would ideally be able to safely recover from out-of-memory errors. This gets tricky, though, because a partial install could put the site/configuration in an invalid state which may be hard to recover from.

Perhaps there should be a flag on RecipeRunner::toBatchOperations() to control this behavior if it does get added? Personally speaking, I prefer stability over speed.

catch’s picture

@clayfreeman there's already a setting to control the number of modules per batch so that sites can control this.

For cli installs it should lower the memory requirements rather than increase it.

clayfreeman’s picture

A value in settings.php could be somewhat inflexible for site builders that don't have access to change the site code. It'd be great if this was part of RecipeRunner::toBatchOperations() so that it could be set when submitting the form, or sourced from a configuration object. Additionally, CLI may have different (looser) memory constraints than other PHP SAPIs based on hosting platform configuration.

alexpott’s picture

@clayfreeman this is not using any different batch sizes than the module install form, drush instlaller, site install via the UI. If you want to propose adding a CLI and UI setting then this need to work for all of those things and not just recipes. For my part, I'm not sure about presenting a UI option to set. How is a user to know they've set the right value? Why would they ever change the default and how would they know?

phenaproxima’s picture

Read through this and it mostly makes sense. I don't quite understand how or why this RecipeMultipleModulesConfigInstaller gets us the behavior we want, though.

alexpott’s picture

Addressed @phenaproxima's comments. To answer #27 because it allows the config storage supplied to the module installer by the recipe runner to read from all the modules we're installing in a safe manner and also do the recipe magic of override config using the recipe's config folder.

thejimbirch’s picture

The current change record covers:

RecipeRunner::installModule() is deprecated
RecipeRunner::installModules() is added

Do we need to document this?

new RecipeMultipleModulesConfigStorage class

Is there anything recipe author/applier facing that should be documented? Or is this all behind the scenes?

phenaproxima’s picture

Status: Needs review » Needs work

Looks like the unit test for the new config storage is failing.

alexpott’s picture

Status: Needs work » Needs review

Fixed the unit test.

@thejimbirch it is all behind the scenes.

thejimbirch’s picture

:thumbs_up:

phenaproxima’s picture

Status: Needs review » Reviewed & tested by the community

I have a couple of nits left but I see nothing, really, to complain about here.

As before, we're still delegating to the module installer, which has always accepted an array of module names to be installed in one go. The real innovation here is the read-isolation of RecipeMultipleModulesConfigStorage, and that is quite thoroughly tested; everything else about it is straightforward.

  • catch committed eedf411f on 11.x
    task: #3498026 RecipeRunner::processInstall() should allow for...

  • catch committed 7410b513 on main
    task: #3498026 RecipeRunner::processInstall() should allow for...
catch’s picture

Title: RecipeRunner::processInstall() installs modules one by one » RecipeRunner::processInstall() should allow for installing multiple modules at once
Version: main » 11.x-dev
Status: Reviewed & tested by the community » Fixed

Last minute title change for a more task: friendly commit message.

Committed/pushed to main and 11.x, thanks!

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.

Status: Fixed » Closed (fixed)

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