Overview

Identified at https://git.drupalcode.org/project/canvas/-/merge_requests/23#note_590788 on #3544604: Calculating dependencies of `ReferenceField(Type)PropExpression` is missing intermediate dependencies.

hook_storage_prop_shape_alter() is basically the only PHP API that Canvas offers.

However, the results of this method may need to be re-evaluated based on external changes that Canvas is unaware of.

Example

\Drupal\canvas\Hook\ShapeMatchingHooks::mediaLibraryStoragePropShapeAlter() implements this hook on behalf of the media_library module.

  1. ✅ A module getting installed in Drupal automatically rebuilds caches. So the hook is executed automatically. Which means Component config entities are updated automatically with the field type+widget that should be used for "image"-shaped props. If there are >=1 MediaTypes using the image MediaSource plugin. If there's zero, Canvas sticks to its default that is guaranteed to work (the image field type+widget).
  2. 🐛 However, if there were 0 such MediaTypes the first time it ran, and then the user creates their first such MediaType, then the site incorrectly stays "stuck" on using the image field type, rather than switching to the media library widget!
  3. 💡 What's missing is the ability for hook_storage_prop_shape_alter() to convey what cache tags cause its results/conclusions to change. In the case of ShapeMatchingHooks::mediaLibraryStoragePropShapeAlter() that's the config:media_type_list cache tag. But this won't cause component config entities to be regenerated for the JsComponent source plugin - it only updates when the JavascriptComponent entity is saved
  4. 💡 We can listen for changes to media source fields using hook_field_config_insert and hook_field_config_update and use that to invalidate discovered SDC components and resave any JavascriptComponents that have props referencing image or video objects. But that requires taking care of each use case one by one, which won't scale when contrib chimes in: there are multiple reasons the shape might need to change

Proposed resolution

  1. 🤓 Make Component config generation/updates predictable (in #3556327: Don't save `Component` config entities unnecessarily)
  2. Introduce a prop shape repository as a cache collector (TBD) with associated cacheability metadata for each one.
  3. If any prop shape changes, the prop shape repository can invalidate the shapes and ask the required component source manager to update the relevant components:
    1. Invalidate discovered SDC components and
    2. resave any JavascriptComponents that have props affected.

User interface changes

See the example above: the 🐛 would disappear.

Issue fork canvas-3547579

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

wim leers created an issue. See original summary.

nagwani’s picture

wim leers’s picture

Also: we should probably prefix the hook name with canvas 😅

wim leers’s picture

This affects #3519247-31: Acquia DAM and Canvas integration too, not only Canvas' own ShapeMatchingHooks::mediaLibraryStoragePropShapeAlter().

wim leers’s picture

Issue summary: View changes

Looks like this won't cause a BC break, just an API expansion. 🎉 The MR already implements half of the proposed resolution.

That means this is safe to land later, and it won't even impact #3519247: Acquia DAM and Canvas integration at all, because that just builds upon ShapeMatchingHooks::mediaLibraryStoragePropShapeAlter() and needs the same cache tag 👍


Next steps here: I think this will need some kind of cache collector service, and then re-trigger Component generation as soon as any of the cache tags is invalidated.
mglaman’s picture

How will a cache miss due to cache tag invalidation cause recalculation of prop_field_definitions? Cache tags ensure cache miss, but nothing about prop_field_definitions works by a missed cached, it is explicitly calculated.

---

I'm not sure collecting this cacheable metadata will solve the problem. The `prop_field_definitions` is calculated inside of the component's config. This is calculated whenever the config entity is created or updated per

- \Drupal\canvas\Plugin\Canvas\ComponentSource\SingleDirectoryComponent::createConfigEntity
- \Drupal\canvas\Plugin\Canvas\ComponentSource\SingleDirectoryComponent::updateConfigEntity
- \Drupal\canvas\Plugin\Canvas\ComponentSource\JsComponent::createConfigEntity
- \Drupal\canvas\Plugin\Canvas\ComponentSource\JsComponent::updateConfigEntity

These invoke `\Drupal\canvas\Plugin\Canvas\ComponentSource\GeneratedFieldExplicitInputUxComponentSourceBase::getPropsForComponentPlugin` and update the `prop_field_definitions` settings which are passed as configuration to the component source plugin.

Canvas is regenerating SDC backed components in \Drupal\canvas\Plugin\ComponentPluginManager::setCachedDefinitions, but there's no way for us to push the cacheable metadata here. And the config changes happen after the cache is set, along with the fact `\Drupal\Core\Plugin\DefaultPluginManager::setCachedDefinitions` sets the cache tags based on the property for the class (okay, I guess we could dynamically alter that during processing.) But if we solved this for SDC's it wouldn't work for Code Components.

We just ran into this with testing out the Acquia DAM module support with image props. We had to re-save our Code Components so that a new version of the component was created that respected the new bundle. Technically all changes to the `prop_field_definitions` should trigger a new version and shouldn't be dynamically updated unless something re-saves/updates the component.

Now, that does make it pretty annoying if you add a new media type and it isn't usable, with no clear way to make it usable (need to trigger your component entity to be re-run with `updateConfigEntity` by its source.)

mglaman’s picture

Assigned: Unassigned » wim leers
phenaproxima’s picture

For transparency's sake - @mglaman asked me to look at this and validate his comment in #6. I have to admit I have the same question; at first blush, it is not clear to me how invalidating the cache tags of (let's say) media types will also trigger a cache tag invalidation for blocks and SDCs, although I could see a way (potentially) to rig that up by refactoring setCachedDefinitions() in both plugin managers.

But where does that leave code components? Those are just entities, so they're not centrally cached anywhere that I'm aware of. How can we possibly trigger an update for them if a new media type is created, apart from just a regular hook_media_type_insert() hook implementation?

If that's the vision, then it would be good to get explicit confirmation about that from Wim.

larowlan’s picture

Yes we had the same issue with automatic regeneration in #3550334: [update path] [upstream] Text formatted with CKEditor within Canvas gets double escaped when output yesterday - i.e. the fact SDC component config entities can be resaved on a cold cache during discovery meant that update hook changes were applied during discovery rather than during the post update hook.

The same applies for code components.

I think yeah we need a hook_media_type_insert/hook_media_type_update here to flag that we need to regenerate code component config entities - we can query for JavascriptComponents with a prop of type `$ref: 'json-schema-definitions://canvas.module/image'` and resave them

I'll have a crack at that

mglaman’s picture

i.e. the fact SDC component config entities can be resaved on a cold cache during discovery meant that update hook changes were applied during discovery rather than during the post update hook.

😱 I didn't even think about the fact UpdateKernel uses NullBackend so the plugin managers are always calling setCachedDefinitions and causing component churn, unless the update hooks mark config as syncing.

I think yeah we need a hook_media_type_insert/hook_media_type_update here to flag that we need to regenerate code component config entities - we can query for JavascriptComponents with a prop of type `$ref: 'json-schema-definitions://canvas.module/image'` and resave them

Makes sense to me. Let's tackle the common use case. We should also tackle video as well.

larowlan’s picture

larowlan’s picture

Status: Active » Needs review

larowlan changed the visibility of the branch 3547579-hook_storage_prop_shape_alter-allow-specifying-when-to-reevaluate to hidden.

larowlan’s picture

Issue summary: View changes
larowlan’s picture

Turns out hook_media_type_insert is too early, because at that point the source field doesn't yet exist. So used hook_field_config_insert and the equivalent update hook instead. Works just fine!

This doesn't cover any contrib-based source plugin that extends from \Drupal\canvas\Plugin\Canvas\ComponentSource\GeneratedFieldExplicitInputUxComponentSourceBase

But I think we can live with that.

larowlan’s picture

😱 I didn't even think about the fact UpdateKernel uses NullBackend so the plugin managers are always calling setCachedDefinitions and causing component churn, unless the update hooks mark config as syncing.

We should consider opening a new issue to prevent this, e.g. by checking if we're in an update kernel in the component manager and block manager before we blindly save new entities - if this happens during database updates we're breaking the rules around what you can do in an update kernel vs what should wait for the post update hooks. Namely, using the Entity APIs is not supported in an update kernel until post update hooks.

larowlan’s picture

larowlan’s picture

Title: Canvas' hook_storage_prop_shape_alter() must allow passing cache tags — to know when to re-evaluate » Changes to media types should force re-invoke of hook_storage_shape_prop_alter
penyaskito’s picture

Assigned: wim leers » larowlan
Status: Needs review » Needs work

Need some feedback for validating the new approach.

wim leers’s picture

#7 + #9: you're right, we'd need to force a re-discovery of all Components. I'd swear we had an issue for moving discovery onto ComponentSourceInterface (one of the million things still to do before that can become a public PHP API for contrib to adopt, see #3520484: [META] Production-ready ComponentSource plugins) , but I can't find an issue for it 😬 → it's in an issue comment: #3526045-7: Settle on final name for `ComponentSource` plugins (`ComponentType`?) + expand their scope (discovery, maybe more?). @mohit_aghera already +1'd that based on his experience building a new component source plugin!

If we do that, then we can explicitly retrigger discovery after cache tag invalidation.

This is one of the reasons that \Drupal\Tests\canvas\Kernel\Plugin\Canvas\ComponentSource\ComponentSourceTestBase::testDiscovery() exists: to ensure that we can evolve (improve!) the discovery mechanism, while yielding the same results.

Doing that will also allow us to remove the awkward hardcoded \Drupal\Tests\canvas\Traits\GenerateComponentConfigTrait::generateComponentConfig() dance, that 100% certainly will not work for contrib, because we've been manually expanding it for Canvas' included sources!


I think that is the right solution, not one-off hooks like MR 298 is adding. That is an utterly unscalable approach IMO, because there could be any number of reasons/factors that require an invalidation, not only new fields being created. And I see that @penyaskito and @larowlan are already discussing that concern on the MR too 👍

That would likely also give us a nicer way to solve #10/#11 aka #3556327: Don't save `Component` config entities unnecessarily.

It requires a bigger refactor, but we already know that the whole component source infrastructure needs significant refactoring. That is why Product decided that we would ship Canvas 1.0 without that as a public PHP API! But turns out that fully digging into this issue reveals that some of it does already need to happen now. So be it.


→I've pushed a commit that demonstrates how we could achieve this: https://git.drupalcode.org/project/canvas/-/merge_requests/145/diffs?com...

wim leers changed the visibility of the branch 3547579-hook_storage_prop_shape_alter-allow-specifying-when-to-reevaluate to active.

larowlan’s picture

Working on this in MR 145

larowlan’s picture

Assigned: larowlan » Unassigned

I'm done for today, but I've moved the needle on MR 145. I've left @todos for breadcrumbs where things need work

larowlan’s picture

Assigned: Unassigned » wim leers
Status: Needs work » Needs review

Assigning to Wim for a sanity check of the current progress/approach before we go any further.

There is certainly more refactoring we can do here - such as

wim leers’s picture

Assigned: wim leers » larowlan
Status: Needs review » Needs work

Assigning to Wim for a sanity check of the current progress/approach before we go any further.

Done: https://git.drupalcode.org/project/canvas/-/merge_requests/145?commit_id...

Basically: YES, this looks right to me! Does it to you as well, @larowlan?

effulgentsia’s picture

Assigned: larowlan » penyaskito

Discussed this in a meeting and @penyaskito will pick up on this where @larowlan left off.

penyaskito’s picture

This needs a Issue summary update, and potential follow-ups for #27.

wim leers’s picture

Can you update the issue summary and create those follow-ups, @penyaskito? 🙏

#27 makes sense, but is fairly open-ended. I think/hope you've had conversations with @larowlan that enables you to better update the IS+create followups than me?

I clarified the follow-up for the first bullet in #27: #3526045-13: Settle on final name for `ComponentSource` plugins (`ComponentType`?) + expand their scope (discovery, maybe more?).

wim leers’s picture

RE: #10 + #11 + … + #18 and the issue that @larowlan created for it: #3556327: Don't save `Component` config entities unnecessarily

Those unconditional Component::save() calls in the (SDC and JS) component sources are causing cache tag invalidations after Dynamic Page Cache has been populated … which is the root cause of what @penyaskito has been debugging for days: https://git.drupalcode.org/project/canvas/-/merge_requests/145/diffs?com...

IOW: +1 for fixing #3556327: Don't save `Component` config entities unnecessarily, and actually this issue requires some of that to be fixed (i.e. the commit above). Specifically, we should only perform actual saves of Components when something actually changed. That's what I described at #3556327-6: Don't save `Component` config entities unnecessarily, and which the linked commit does.

wim leers’s picture

Assigned: penyaskito » wim leers

That linked commit alone is not enough to make the tests pass.

Fixing it is a slippery slope towards more complexity. Because a bit of foundation is missing.

Working to add that. To be continued tomorrow.

wim leers’s picture

Title: Changes to media types should force re-invoke of hook_storage_shape_prop_alter » [PP-1] Changes to media types should force re-invoke of hook_storage_shape_prop_alter
Assigned: wim leers » penyaskito
Status: Needs work » Postponed

The problems this MR are running to that I've alluded to in #32 and #33, but now called out explicitly:

  • the saving of Components during discovery, even if nothing has changed, is the root cause for why @penyaskito was debugging these tests for several days
  • working around it is seemingly simple, but causes other failures
  • those other failures may in theory be fixable, but the logic is so incredibly spread out that doing that is very risky: we're dealing with interactions here of:
    1. component source plugin
    2. associated decorated plugin manager or config entity storage, which triggers Component::save()s
    3. 👆 those 2 are already wildly inconsistent in how they interact with each other, and which is responsible for what AND each component source plugin implementation + associated decorator does things differently
    4. a cache collector's updateCache() method calling upon destruction (i.e. during KernelEvents::TERMINATE)
    5. a mind-boggling amount of cyclomatic complexity

⇒ this re-surfaces/stresses the need for making "component sources" a solid, supported, public PHP API. But that is out of scope for 1.0. Yet we want to tag 1.0, and have 1.0 be maintainable. So even though we should do #3556327: Don't save `Component` config entities unnecessarily and really all of #3520484: [META] Production-ready ComponentSource plugins, we can't.

⇒ tackling the foundational parts in #3556327, see #3556327-9: Don't save `Component` config entities unnecessarily. I'm already well on my way: https://git.drupalcode.org/project/canvas/-/merge_requests/378. @penyaskito is already reviewing. 👍

penyaskito’s picture

Issue summary: View changes
wim leers’s picture

Issue tags: +Needs tests

While working on #3556327: Don't save `Component` config entities unnecessarily's !378 MR, I stumbled upon \Drupal\Tests\canvas\Kernel\Config\ComponentTest::testComponentAutoUpdate(), which is literally about what this issue is describing:

  /**
   * @see media_library_storage_prop_shape_alter()
   * @see \Drupal\Tests\canvas\Kernel\MediaLibraryHookStoragePropAlterTest
   */
  public function testComponentAutoUpdate(): void {

This MR should update that test.

wim leers’s picture

phenaproxima’s picture

Title: [PP-1] Changes to media types should force re-invoke of hook_storage_shape_prop_alter » Changes to media types should force re-invoke of hook_storage_shape_prop_alter
Status: Postponed » Needs work

Blocker's committed.

penyaskito’s picture

Assigned: penyaskito » Unassigned
Status: Needs work » Needs review
effulgentsia’s picture

The main reason we were trying to get this resolved before the 1.0.0 release is that it's making a breaking change to a hook that's documented in canvas.api.php. However, we can document in #3553397: Document our @internal API policy that this isn't a public API yet.

Also, someone could run into this bug when adding media types after installing Canvas. But, even though that's annoying, a full cache clear resolves it.

So, downgrading this from a stable blocker to a post-stable priority. This issue is close. We'll hopefully get it fixed next week and can release a 1.0.1 or 1.1.0 with it.

penyaskito’s picture

Assigned: Unassigned » penyaskito
penyaskito’s picture

Title: Changes to media types should force re-invoke of hook_storage_shape_prop_alter » Introduce a new cache tags aware prop shape repository, so changes affecting prop shape calculation can force the re-invoke of hook_canvas_storable_shape_prop_alter
Assigned: penyaskito » wim leers
Issue tags: -Needs issue summary update, -Needs followup, -Needs tests

Re-titling. Issue summary was already updated.
This has tests. All follow-ups exist already.

Assigning to Wim as he is very familiar with this space and with the MR itself.

wim leers’s picture

Status: Needs review » Needs work
wim leers’s picture

Assigned: wim leers » penyaskito
Status: Needs work » Needs review

Back to @penyaskito for final review, because I made too many changes to be able to just go ahead and merge.

The key things I did:

  1. Tests weren't passing due to a small bug in the excellent new test coverage @penyaskito wrote.
  2. simplified
  3. Quite a significant refactor of (ReadOnly)PropShapeRepository, to (Persistent|Ephemeral)PropShapeRepository. See details+reasoning+walkthrough.

Remaining for @penyaskito:

  1. review that refactor
  2. update SingleDirectoryComponentTest::testDiscovery() expectations
  3. https://git.drupalcode.org/project/canvas/-/merge_requests/145#note_634958 needs to be answered
  4. https://git.drupalcode.org/project/canvas/-/merge_requests/145#note_638367 needs to be addressed, and in doing so should make https://git.drupalcode.org/project/canvas/-/merge_requests/145#note_638364 disappear
  5. restore canvas.api.php: https://git.drupalcode.org/project/canvas/-/merge_requests/145#note_638555
  6. considered out of scope: address https://git.drupalcode.org/project/canvas/-/merge_requests/145#note_638497

EDIT: next morning, I followed through on the ones I could.

wim leers’s picture

  1. Ran into obstacles while getting this green: https://github.com/phpstan/phpstan/issues/13566#issuecomment-3645405380
  2. Created a diagram for how all this works, because if we want to open up this API, then I think having a diagram to make this more widely understood without having to read code is required for sanity. Sadly, GitLab's lagging in its Mermaid version (10, 11 has been out for over a year), so it's best viewed on mermaid.live — until GitLab updates, that is!
wim leers’s picture

Status: Needs review » Reviewed & tested by the community

Ready for final sign-off. Followed through on all next steps I listed in #44, and added much-needed docs/diagrams :)

P.S.: do we also want to update /API.md? I'd be fine leaving that for a follow-up, to allow that to be its own conscious decision.

penyaskito’s picture

I'll create a follow-up to update /API.md

UPDATE: #3562890: Document PHP APIs in API.md


Another epic effort (+100 commits! +1007 −295!). This is a huge improvement overall (also in terms of performance), with a small penalty for code components vs SDCs on re-discovery, which we already have follow-ups to alleviate (e.g. #3561272: Add the ability for ComponentSourceManager to generate a Component config entity for a single source, or even a single specific component).

As this will trigger _less_ rediscoveries, this is an improvement over-all!

penyaskito’s picture

Published the 3 change records.

penyaskito’s picture

Assigned: penyaskito » Unassigned
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.

Status: Fixed » Closed (fixed)

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