Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid unnecessary identity notification when record is saved #8566

Merged
merged 8 commits into from
May 6, 2023

Conversation

robbytx
Copy link
Contributor

@robbytx robbytx commented Apr 13, 2023

Description

Since the elimination of InternalModel in 4.7.0, every time an existing record is updated, the Record Data/Cache (by invoking InstanceCache.setRecordId) notifies that the identity of the record has changed, even if the id was not changed, which is the typical case.

This behavior leads to unnecessary invalidation (and therefore recomputation) of properties that are subscribed to model.id, leading to worse performance and possible bugs due to the unexpected behavior.

Notes for the release

N/A?

@robbytx
Copy link
Contributor Author

robbytx commented Apr 13, 2023

@runspired I feel like the IdentifierCache.updateRecordIdentifier method (or each caller of it) should be responsible for notifying the identity property, so that the notifications are consistently sent precisely when the id changes.

An alternative is to revert to the earlier behavior where the cache tracks the id itself so it can detect when that changes.

A possible third alternative is to eliminate IdentifierCache.updateRecordIdentifier call that precedes Cache.didCommit, so that it can actually observe the id change.

@runspired runspired added 🎯 release PR should be backported to release 🎯 canary PR is targeting canary (default) 🎯 lts The PR should be backported to the most recent LTS 🏷️ bug This PR primarily fixes a reported issue 🎯 beta PR should be backported to beta labels Apr 14, 2023
@runspired
Copy link
Contributor

runspired commented Apr 14, 2023

I feel like the IdentifierCache.updateRecordIdentifier method (or each caller of it) should be responsible for notifying the identity property, so that the notifications are consistently sent precisely when the id changes.

Possibly, though I still think we want the cache to determine the right time. The issue here is the legacy-handler, were it not for the legacy support the cache would be what calls updateRecordIdentifier.

An alternative is to revert to the earlier behavior where the cache tracks the id itself so it can detect when that changes.

I think the 4.4 behavior was due to some craziness in how InternalModel specifically buffered things.

A possible third alternative is to eliminate IdentifierCache.updateRecordIdentifier call that precedes Cache.didCommit, so that it can actually observe the id change.

broad strokes yes. If we can make this happen today I'm for it, but I suspect the issue with this is legacy support / deprecated code-path support.

There's another angle here: you shouldn't actually be allowed to change the ID once it is set, but this is not a constraint of the identity-manager but rather a constraint of the cache. So the cache should throw an error if an id exists and a new id is returned.

Today, we warn in the cache when a new ID is returned

    if (id !== undefined) {
      let newId = coerceId(id);

      if (identifier.id !== null && identifier.id !== newId) {
        // here we warn and ignore, as this may be a mistake, but we allow the user
        // to have multiple cache-keys pointing at a single lid so we cannot error
        warn(
          `The 'id' for a RecordIdentifier should not be updated once it has been set. Attempted to set id for '${wrapper}' to '${newId}'.`,
          false,
          { id: 'ember-data:multiple-ids-for-identifier' }
        );
      }
    }

We should also only update the id on the identifier if none existed before or if the configured update handler tells us too, e.g. this appears too greedy

   // upgrade the ID, this is a "one time only" ability
  // for the multiple-cache-key scenario we "could"
  // use a heuristic to guess the best id for display
  // (usually when `data.id` is available and `data.attributes` is not)
  if ((data as ExistingResourceObject).id !== undefined) {
    identifier.id = coerceId((data as ExistingResourceObject).id);
  }

@robbytx
Copy link
Contributor Author

robbytx commented Apr 14, 2023

There's another angle here: you shouldn't actually be allowed to change the ID once it is set, but this is not a constraint of the identity-manager but rather a constraint of the cache. So the cache should throw an error if an id exists and a new id is returned.

I'm hesitant to introduce that change, since this has not been a hard requirement in the past, and it strikes me as reasonable to permit model ids to change over time, at least in well-defined ways that have worked in the past.

If the behavior has worked, and it serves a use case, then I feel like it go through an official deprecation + major release before being removed.

I suspect the issue with this is legacy support / deprecated code-path support.

I'm reading this and I see that the file is called "legacy", but I don't see any replacement code path that invokes didCommit, so to me this is the primary/non-legacy code path for all intents and purposes, until a working alternative is available.

The issue here is the legacy-handler, were it not for the legacy support the cache would be what calls updateRecordIdentifier.

But the cache does call it! The legacy handler invokes cache.didCommit, which invokes InstanceCache.setRecordId (via its store wrapper), which invokes IdentifierCache.updateRecordIdentifier.

So why can't we remove the legacy handler's up-front call to IdentifierCache.updateRecordIdentifier?

  1. Is it because there are existing, supported cache implementations that don't call setRecordId? This would be surprising to me, but I suppose possible. In this case, we could vary behavior based on whether the cache is default.
  2. Is it because we need to handle the merge before passing to the cache, so the cache updates the right target? In this case, I don't see how an alternate code path could avoid doing the same.
  3. Is it because the legacy handler needs the actualIdentifier in order to do store.peekRecord(actualIdentifier)? Why can't it ask the identifier cache to translate the original identifier to whatever the merge result might have been?
  4. Is it because there's a timing issue with handing it off to the cache that I'm overlooking?

@runspired
Copy link
Contributor

runspired commented Apr 15, 2023

@robbytx

I'm hesitant to introduce that change, since this has not been a hard requirement in the past, and it strikes me as reasonable to permit model ids to change over time, at least in well-defined ways that have worked in the past.

This actually was a hard constraint going very far back. It was accidentally relaxed when we removed InternalModel, InternalModel would not have allowed this to occur.

I'm reading this and I see that the file is called "legacy", but I don't see any replacement code path that invokes didCommit, so to me this is the primary/non-legacy code path for all intents and purposes, until a working alternative is available.

That's because the alternative is literally "this thing does not need to exist" 😅

So why can't we remove the legacy handler's up-front call to IdentifierCache.updateRecordIdentifier?

We may not need to anymore, this needs investigated. In the recent past the answer would have been a combination of reasons (2) (3) and (4).

Your solution in (3) sounds correct, but I think this shows that didCommit should be changed to return the response document similar to put. I'll put up a PR that makes that addition (and amend the RFC)

e.g.

didCommit<T>(
    identifier: StableRecordIdentifier,
    data: StructuredDataDocument<T>
  ): void;

should be

didCommit<T>(
    identifier: StableRecordIdentifier,
    data: StructuredDataDocument<T>
  ): ResourceDocument;

@runspired runspired added the lts-4-12 Long Term LTS Maintenance label May 6, 2023
@runspired runspired merged commit 894bdfe into emberjs:main May 6, 2023
@runspired runspired removed the lts-4-12 Long Term LTS Maintenance label May 24, 2023
runspired added a commit that referenced this pull request May 24, 2023
* Add failing test

* Ignore JetBrains IDE files

* Revise failing test

* Call setRecordId only for new records

* Add (failing) test for updated id from server

* update test

* error when appropriate, thread context to give cache responsibility

* fix lint

---------

Co-authored-by: Chris Thoburn <[email protected]>
runspired added a commit that referenced this pull request Jun 10, 2023
* Add failing test

* Ignore JetBrains IDE files

* Revise failing test

* Call setRecordId only for new records

* Add (failing) test for updated id from server

* update test

* error when appropriate, thread context to give cache responsibility

* fix lint

---------

Co-authored-by: Chris Thoburn <[email protected]>
runspired added a commit that referenced this pull request Jun 10, 2023
* fix: restore Store extends EmberObject :( (#8594)

* fix: restore Store extends EmberObject :(

* fix EmberObject bs

* fix: LegacyHandler should provide unfrozen options to Adapters (resolves #8576)


---------

Co-authored-by: Chris Thoburn <[email protected]>

* fix: dont share promise cache for all fields (#8597)

* fix: docs generation should maintain a stable relative path (#8598)

* docs: fix forgotten references to FetchManager (#8601)

* Avoid unnecessary identity notification when record is saved (#8566)

* Add failing test

* Ignore JetBrains IDE files

* Revise failing test

* Call setRecordId only for new records

* Add (failing) test for updated id from server

* update test

* error when appropriate, thread context to give cache responsibility

* fix lint

---------

Co-authored-by: Chris Thoburn <[email protected]>

* fix backport of identity notification fix

* There are cases where payload is an object, and normalizeErrorRespons… (#8621)

* There are cases where payload is an object, and normalizeErrorResponse ends up returning [object Object] for "detail"

* Add comment about JSON.stringify

* Update rest.ts

---------

Co-authored-by: Maxim <[email protected]>
Co-authored-by: Robby Morgan <[email protected]>
Co-authored-by: NullVoxPopuli <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🎯 beta PR should be backported to beta 🎯 canary PR is targeting canary (default) 🎯 lts The PR should be backported to the most recent LTS 🎯 release PR should be backported to release 🏷️ bug This PR primarily fixes a reported issue
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants