Problem/Motivation
Block plugins are used in core in three different modules:
1. Block module (config entities)
2. Layout builder (config entities and also view mode overrides in content).
3. Navigation (a single config object - navigation.block_layout, but using the layout builder data structure)
Also, in Experience Builder - see #3520484: [META] Production-ready ComponentSource plugins and #3501708: Prove that it *will* be possible to apply block settings update paths (assuming #3521221 in core) to stored XB component trees in config/content which inspired this issue.
When a block plugin updates its settings structure, all of these places should be updated, but, this is not the case.
As far as I can tell, there are two reasons this hasn't come up before:
1. We rarely update block plugin settings so have got lucky that we haven't fatally broken any sites yet.
2. We don't validate block plugin settings config in these various places yet.
/**
* Updates Search Blocks' without an explicit `page_id` from '' to NULL.
*/
function search_post_update_block_with_empty_page_id(&$sandbox = []) {
$config_entity_updater = \Drupal::classResolver(ConfigEntityUpdater::class);
$config_entity_updater->update($sandbox, 'block', function (BlockInterface $block): bool {
// Only update blocks using the search block plugin.
// @see search_block_presave()
if ($block->getPluginId() === 'search_form_block' && $block->get('settings')['page_id'] === '') {
$settings = $block->get('settings');
$settings['page_id'] = NULL;
$block->set('settings', $settings);
return TRUE;
}
return FALSE;
});
}
This is updating block config entities, but not layouts or navigation.
Search module theoretically could add additional updates for layouts and navigation, and also update their config with the same changes, but it doesn't. And even if it did do that, search module can't update experience builder, because that's in contrib, or any other theoretical place that consumes block plugins and stores their settings.
Some of this is going to be quite painful to implement, but I think conceptually it ought to work.
We will also need to change the search update to use this new, correct method, and document this very clearly somewhere too. It's like an extra level of update hell on top of normal config entity updates which everyone already gets wrong all the time.
Steps to reproduce
Proposed resolution
I think we need to add an API for updating block plugin settings.
It would look something like this - very rough:
BlockPluginSettingsUpdaterInterface {
isUpdateNeeded(array $stuff): bool;
updateStuff(array $stuff): array;
}
This is a kind of formalization/abstraction of what ViewsConfigUpdater does.
Search module's update would then look something like this:
class SearchEmptyPageUpdater implements BlockPluginSettingsUpdaterInterface {
// logic here
}
<?php
hook_post_update_NAME(&$sandbox) {
\Drupal::service('block_plugin_updater')->updateBlockPluginSettings('search', 'SearchEmptyPageUpdater::class', $sandbox);
}
The new block_plugin_updater service then triggers an event/hook, and... this bit is fuzzy, layout builder, block, navigation, experience builder, return a callback or something, that does what they would normally do in their hook_update_N().
Either they need create a batch that does the update (which would end up as a nested batch inside the post update hook), or return enough information for the block_plugin_updater class to do that.
Conversely, updates are not the only place that this logic needs to run, it also needs to run on presave of all these different config entity types to support shipped config too.
Soo SearchEmptyPageUpdater and its equivalents, as well as implementing the interface, could also be a plugin. Then the various hook_entity_presave() can call another core service, which takes a config entity, discovers all the blocksettingsupdater plugins, then runs that same logic to update the config entity before it's saved. Or maybe not via a service and do it directly since they're going to be responsible for determining where in their config structure the settings live and passing that to the updater plugins.
To try to summarize:
Modules that provide block plugins, when they update the settings structure, need to provide a 'block plugin settings updater' plugin implementing an interface with the logic to detect if an update is needed and also update the data structure. And they also need to implement a hook_post_update that calls a service method, passing the plugin to it.
Modules that consume block plugins and store their settings, need to implement a hook_entity_presave(), and in that presave, call a service (or whatever), that gets all the block plugin settings updater plugins and runs their logic.
Alternate proposal
See #29
Remaining tasks
Figure out what approach to take.
Perform clean up of comments linking to this issue such as in ItemsPerPageTest. See #33.
User interface changes
Introduced terminology
API changes
Data model changes
Release notes snippet
Issue fork drupal-3521221
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
catchComment #3
catchComment #4
catchI haven't looked at Dashboard module internals, but pretty sure that also needs the same treatment.
Comment #5
catchComment #8
catch@longwave mentioned in slack the possibility of generalising this to all plugins that have settings. We could maybe use it for views config updates if we did that.
Also config entities that store plugin settings could maybe generically do something to, to apply any plugin update plugins they need to. Although they also need to indicate where those plugin settings are stored in their own data structures. Only views knows where area or filter plugins are in its own config entities.
Comment #9
catchFor scope something like:
1. Add the update plugin interface, and the updater service called from post updates, and the presave bit, and implement this in block module + search for the update that already exists. That will give us one working implementation of one update and one block plugin storage. We'll need to decide if we make this generically useful for all plugin types or limit it to blocks.
2. Open separate issues for layout builder, navigation, and experience builder to implement the storage update parts.
3. Check for any other block updates in core that follow a similar pattern to the search one and convert those too.
4. If we do make this generically useful for other plugin types and config entities, open follow-ups for those.
Comment #10
penyaskitoConfirm, we would need to do the same, and we are not doing anything for this scenario (in this case I'd expect layout builder implementation to handle this for everyone using a SectionStorage plugin).
IMHO this will be complex enough that it could be left for a follow-up, and AFAIK we have no precedent of this. Looks like a related-but-not-the-same separate beast.
E.g. if node module implements a config schema change on node_type, the shipped node types config of contrib module X are not updated on installation of module X.
If we include shipped config on the scope, and per my comment above, wouldn't this be a general problem for any config that has an update to its structure/config schema, and not only plugins?
And I'd expect we would need some kind of tracking of the last hook_update_N/post_update where this shipped config was generated with, which might mean a huge change with no BC layer.
Comment #11
catchWe already implement an equivalent presave hook for every* config entity update in core. Views has the most examples of this, which are handled centrally via ViewsConfigUpdater::updateAll(). The search example above also comes with a presave hook. See https://api.drupal.org/api/drupal/core%21modules%21search%21src%21Hook%2...
*except when we forget, although I think Drupal 10/11 are better than 8/9 were.
This doesn't necessarily go against doing this in two issues, but it would probably be worth seeing what things look like when we're a bit further along here - because we already have the equivalent logic to convert to the new thing.
Comment #12
acbramley commentedWe're starting to tackle that now with pre_save hooks (see issue below)
I've opened #3521618: Add generic interface + base class for upgrade paths that require config changes which is sort of similar to this issue but for config entity changes specifically.
WRT. this issue, Layout builder is going to be really tricky here for overridden entities. That config is stored as a blob in the database. It's a massive headache to update these and requires an entity save or some DB voodoo.
I wonder if instead we have something that applies default config upon loading the plugin? That way when a user edits an entity that has a block plugin in its overridden LB storage, it'll get those config changes applied and then when they save the changes will persist to the db?
Comment #13
catchThis is true and will also affect the experience builder field in a similar way - however I think we can add the config elements of this, and look at 'plugin configuration stored in entity data' in follow-ups. Just the config aspects are bad enough.
A just-in-time update sounds interesting although wouldn't that then mean the update code needs to be maintained indefinitely?
Comment #14
wim leersComment #15
catchReplying to myself here:
I thought about this some more, and we already sort of have a pattern for just-in-time updates in the phpass module. For phpass we cannot bulk update user password hashes, otherwise we could reverse engineer people's passwords, so we just have to wait for users to log in again, update the password hashes then, and then one day site will uninstall the module and any users that didn't log in for years will need to reset their password.
For content we could bulk update content, but we don't really want to do this in a mandatory hook_post_update_NAME() because we don't want to enforce potentially hours of downtime on sites with lots of content in a specific minor release.
So what we could do is implement a just-in-time update on entity presave, and that will update content one by one when it's updated.
We've then got the question of what to do with content that isn't manually updated, there are several ways to tackle this.
Ideally we'd have a way to detect which entities and revisions need updating, e.g. at least only those that are using layout builder overrides or whatever the criteria is. And once it's loaded the entity, we should be able to check if anything actually needs saving before saving it. This would reduce overall churn.
For actually running the update there are several options:
1. Drush or console command which can be manually run, loads and saves everything that needs an update.
2. Admin page that does this in a batch.
3. Admin page that queues all the content on the site, or does something similar to search indexing with a watermark + cron.
We also have issues around discussing revision compression/pruning so e.g. if we add a way to prune old non-default revisions, or very old default revisions, that would allow sites to cut down on the amount of content that needs to be updated.
This does imply keeping the just-in-time update path around for a couple of major releases, but that code will be easier to maintain than a hook_post_update_NAME(), which can go horribly wrong (all of Drupal 7 and the first few releases of Drupal 8 testify to this).
Comment #16
longwave> a just-in-time update on entity presave
This also has to take place on entity load, because we need to fix up data so it can be handled correctly by anything that wants to consume it - the renderer or whatever else needs the entity; it might never even be saved again without some kind of bulk/batch/queue mechanism. But doing all these checks on load likely comes with a performance penalty, especially if the amount of updates grows over time.
Comment #17
catchOuch, yeah was thinking in terms of config entity presaves but that relies on them being saved before they're loaded, which isn't applicable here for the exact reason we're discussing this at all.
If it's on hook_entity_load() or an equivalent spot, with a lot of isset() checks etc., then it would be cached in the persistent entity cache for most entity types, so it might not be too bad purely in terms of performance impact.
Comment #18
wim leersFascinating! 🤓
Indeed! That's where #3523841: Versioned Component config entities (SDC, JS: prop_field_definitions, block: default_setting, all: slots for fallback) + component instances refer to versions ⇒ less data to store per XB field row is part of the solution, or Experience Builder at least — to know when either a component has changed its "explicit input schema" (SDC: props defined in JSON Schema, block plugin: default configuration and config schema — these require an update path to be provided by the module that provides them), or when the way that a site decides to store data for an SDC (e.g. change which field type is used for storing images — this requires XB itself to figure out the update path — related: #3509115-8: [META] Plan to allow editing props and slots for in-use code components).
@longwave asked what I was going to ask in #16, and #17 kind of blows my mind 🤯😄
Comment #19
wim leersFYI: test coverage is under way at #3501708: Prove that it *will* be possible to apply block settings update paths (assuming #3521221 in core) to stored XB component trees in config/content, which should help vet whatever we come up here — because the test infrastructure that introduces should be trivially updatable to first expect component instance validation errors (and rendering failure) as they do now, but then after applying the update path infrastructure that this issue adds, they should expect success.
Comment #20
berdirInteresting ideas, but as always, things get a bit more complicated with contrib, with the main challenge being release management/timing where an event invoked by core might be not be the right solution.
A practical example is when we deprecated and replaced the node_type condition plugin with the generic entity bundle plugin derivates. Core provided an upgrade path for block configs. pathauto also uses condition plugins to control which patterns apply to which entity. I did write an update function there, but there were still a bunch of people that did run into problems because they either had old default config in install profiles/modules, might have updated pathauto before core, or reimported fixed config after running updates (a surprisingly common issue).
A solution where a block plugin can trigger an update for all possibly afffected places is IMHO extremely hard, because it would require that people update all contrib and custom code at the same time as core introduces that new update, so those implementations actually get triggered. I think that's not realistic and more complex solutions like discovering those conversion handlers/plugins doesn't seem easier than each implementation writing its own update function. Maybe it could be useful to have some apis on plugins to deal with the conversion, but that won't handle a case when a whole plugin gets deprecated, I guess.
Also, what navigation does with block plugins isn't even config entities, just a single simple config file, so generic config entity based approaches wouldn't work there (fwiw, I think what navigation does is wrong, you shouldn't manage block config in simple config, but that's a different topic)
Comment #21
catchYes I think this would be hard, but also this is why #15-#17 are talking about implementing just in time updates - e.g. an API for dealing with old to new conversions that if necessary can run on entity load. Everywhere that needs it would need to implement that API, but it wouldn't be triggered in response to a one-time event.
The problem with that is just-in-time can easily turn into too-late when
the suez canal gets blockedthe specific update code gets removed, so we'd either need to keep these around a lot longer than we currently do for hook_update_N() or add some kind of catch-all, on-demand bulk updater along the lines of search indexing - load all the entities on a site, check if they need updating, update them if they do, but something that can be run via drush/batch as a last resort prior to a major update, not via the regular update system.Comment #22
longwaveThere is an additional hard problem in the case of interdependent entities: if content of entity type A depends on some content of entity type B, and both receive some kind of structure update using this mechanism, which order do the updates run in? Does the update code for entity type A have to deal with the old or new structure of entity type B? Perhaps this is a hypothetical issue for now but as @berdir mentioned contrib I am sure we will run into this case sooner or later, if what we are effectively trying to here is provide long term BC for data structures.
Comment #23
catchOpened #3530727: [meta] Just in time updates for the broader discussion about just-in-time updates.
Comment #24
penyaskitoThis surfaced again in Dashboard's #3557882: Support "placeholder" dashboard blocks.
Comment #25
godotislateHad a thought on how the just in time updates for blocks could work, kinda derived from the database updates system:
SettingsUpdate, which has one propertyschemaVersionand make the attribute applicable to methodsschema_versionas a property on theblock_settingsconfig schema, and set it asrequiredKey: falseSettingsUpdateattribute, and invoke any of those methods whoseschemaVersionis greater than theschema_versionstored in the block config, or run all SettingsUpdate methods if there is noschema_versionstored in the block configSectionComponent::getPlugin()?schema_versionis updated to matchI think the check for and running outstanding
SettingsUpdatemethods would have to occur on every block load, so there might be a performance consideration.Comment #27
wim leersRelated: SDCs may gain new required props.
Canvas MUST solve tackle this. Issue: #3568602: Handle upgrading and rendering not yet upgraded component instances of components with new(ly) required props that include an example value.
Comment #29
godotislateI've opened draft MR 14553 as a PoC of #25.
Some notes:
Attribute class is
ConfigUpdateinstead ofSettingsUpdate, but whatever, naming things is hardupdateSchema) is called fromBlockPluginTrait::setConfiguration, which runs every time a block plugin is instantiatedupdateSchemacould possibly just be called fromBlockPluginTrait::__constructinstead, not sureBut I think the DX would be a little worse
In the MR, I have created a test block plugin
test_block_display_messagein test moduleblock_test_config_update. It has one text propertydisplay_message, and when the block is viewed, it displays the content of that property, as well as a form (more on that in a bit).Based on a property in keyvalue, the class implementing the block plugin is swapped and the block config schema is changed as well. There are two alternate classes:
TestDisplayMessageConfigUpdateBlockwhich adds aConfigUpdatemethod and a second text property that gets displayed.TestDisplayMessageConfigUpdate2Blockwhich adds twoConfigUpdatemethods and another text property that gets displayed. In addition, thesetThirdDisplayMessage()config update method removes the config property introduced by TestDisplayMessageConfigUpdateBlock. The intent of thechangeDisplayMessage()config update method is demonstrate that it does not run if the schema was already updated and saved as a higher version fromTestDisplayMessageConfigUpdateBlockSo initially when the block is placed in any of the global block layout, navigation layout, LB defaults, or LB overrides, the contents of the 'display_message' property is shown along with a form that shows an "Update block config schema" button. When pressed, the button sets the keyvalue property to switch the block plugin class to
TestDisplayMessageConfigUpdateBlockand flushes all caches. After that happens, the display of the block is updated to show the content of thedisplay_message propertyas well as the addedsecond_display_messageproperty whose value was set in thesetSecondDisplayMessage()config update method. In addition, the button has been changed to "Update block config schema again". If pressed, block plugin class is changed toTestDisplayMessageConfigUpdate2Block, which removes thesecond_display_message property,and adds and sets thethird_display_messageproperty. Note that none of these property changes are saved without explicitly saving the block.Last couple notes:
Comment #30
godotislateMoving to NR to see if #29/MR 14553 is a good way to go, high level. Tagging for framework manager review since component is base system (should it be block?) and affects blocks/navigation/layout builder.
Comment #31
godotislateSome follow up thoughts from #29 to deal with Layout Builder components, in overrides mostly:
{base_plugin_id}:{version}updatesproperty to theBlockplugin attribute (instead of the per method attribute), that would look like{base_plugin_id}:{version}strings; compare those with the latest registered versions, then add entities that need updates to a queue, which could be run in a batchComment #32
ghost of drupal pasttear them apart
let block settings be something @block.plugin_settings.$plugin_id.$uuid or some such, those are config objects, block manager can read them, hook_update_N has one place to update them all
Comment #33
smustgrave commentedStumbled upon this in a random test that links here ItemsPerPageTest so can that code be removed or todo updated?
Comment #34
godotislateThe current MR is just a PoC to get consensus on the approach/architecture. Details like that can be updated once we decide on how to proceed.
Comment #35
godotislateComment #36
smustgrave commentedGotcha, my bad
Comment #37
godotislateComment #38
godotislate#36: No worries. It'll likely be a while before this lands, so I added a line to the Remaining tasks in the IS to clean up any code references to this issue.
Comment #39
wim leersNice start, @godotislate! 👏 A few questions on the MR :)
Comment #40
godotislateSo more thinking about what do about thousands or millions of content entities that have block config in them. I'm not too familiar with how Canvas works, so again using Layout Builder as the example:
I think trying to query for the schema version for a block plugin the serialized blob in LB overrides field table is probably a dead end. So I'm wondering instead if there should be an event that can be used in update hooks. Something like:
The Block module could have a subscriber that simply loads all block entities that use the block plugin and resaves them.
LB subscriber would run a query in overrides tables for the block ID, then add the entity to a queue. Queue would be processed during cron, or maybe a UI using the batch API. For defaults, it would load LB enabled displays, load the layout, then resave.
Some questions remain about how to handle forward vs default revisions, etc.