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

readQuery after cache evict #6325

Closed
danReynolds opened this issue May 20, 2020 · 12 comments
Closed

readQuery after cache evict #6325

danReynolds opened this issue May 20, 2020 · 12 comments
Assignees
Milestone

Comments

@danReynolds
Copy link
Contributor

danReynolds commented May 20, 2020

Hello! I have a question about how eviction of normalized objects interacts with queries that already had references to those objects. For example, if I were to do the following:

  const employeesQuery = gql`
    query {
      employees {
        data {
          id
          employee_name
        }
      }
    }
  `;

  const employeesResponse = {
    employees: {
      __typename: "EmployeesResponse",
      data: [{ id: 1, employee_name: 'Test', __typename: 'Employee' }],
    },
  };

cache.writeQuery({
  query: employeesQuery,
  data: employeesResponse,
});

I could then access that data like this:

cache.readQuery({
  query: employeesQuery,
});

but if I then evict the entity that the query contains:

cache.evict('Employee:1')

then subsequent calls to readQuery for employeesQuery fail with error Dangling reference to missing Employee:1 object and the query seems to now just be completely inaccessible and there doesn't seem to be a way to clear out the bad ref that it holds so that readQuery will work again. Is there something I'm missing?

it makes sense that the query would now have missing data, but there seems to be no way to remove it from the query since first I'd need to call readQuery to get what's currently there and then writeQuery to remove the dangling object. If it was able to return partial data then it would work, but reads prohibit that here:

returnPartialData: false,

This forces users to have to refetch queries that have had entities evicted, which can introduce unnecessary network burden if instead the user knows they can remove the evicted entity from that query.

Let me know what I should do to enable accessing or updating those stale queries, thanks!

@hwillson hwillson added this to the Release 3.0 milestone May 21, 2020
@hwillson
Copy link
Member

@danReynolds can you confirm which @apollo/client beta version you're using?

@danReynolds
Copy link
Contributor Author

danReynolds commented May 21, 2020

@hwillson this is on 3.0.0-beta.49. I think the read not supporting partial data is intended behavior, I'm just not sure how we can access fields containing dangling refs then. It'd be nice if evict removed dangling refs, although that might hurt performance so the easiest thing to do I think would be to not return dangling refs in the query response but still allow the query to succeed either with an optional returnPatialData flag, which would be possible if readQueryFromStore made it an option or by default

@danReynolds
Copy link
Contributor Author

@hwillson Here's a commit off latest master demonstrating the problem: danReynolds@d2a09a7, if you run it you'll see it errors out with the dangling reference problem. The goal is to be able to make the query valid again by removing the evicted reference.

@dodas
Copy link

dodas commented May 23, 2020

What about removing entities from queries before evicting them?
I thought the correct way is to update the cache to remove the items I don't want and then evict them / or gc the whole cache.

@danReynolds
Copy link
Contributor Author

What about removing entities from queries before evicting them?
I thought the correct way is to update the cache to remove the items I don't want and then evict them / or gc the whole cache.

ah because that's more manual effort than necessary, any watch queries containing an entity that has been evicted will automatically refetch if partialRefetch is on, which is the desired behavior I'd want versus having to go through all possible queries that could contain it.

@hwillson hwillson assigned hwillson and benjamn and unassigned hwillson May 26, 2020
benjamn added a commit that referenced this issue May 29, 2020
Question: When cache.read{Query,Fragment} encounters a dangling reference
(one that does not currently refer to any normalized entity object in the
cache), should the cache behave as if the (nonexistent) target of the
reference was an empty object, and report any requested fields as missing,
or should the cache generate some other kind of error that reflects the
the absence of the whole object?

I think we could answer this question either way, but I'm leaning towards
the first option.

Note: it's normal for the cache to end up with dangling references when
whole entity objects are evicted, or when a reference is created (e.g. by
toReference) without writing any data into the cache. Cleaning up dangling
references is a tricky problem, requiring application-level reasoning, and
not even always desirable, since the data could always come back into the
cache later, restoring the validity of the reference.

Previously, I thought it would be helpful to distinguish between the
absence of an entity object and the object simply being empty, but I no
longer think this distinction (which only affected the wording of the
MissingFieldError description) matters very much, and we can simplify
everything by adopting the following policy:

> During cache reads, a dangling Reference should behave as much as
  possible like a Reference to an entity object that happens to contain
  zero fields.

I'm optimistic this policy may help with issues like #6325. At the very
least, this policy means there's now only one kind of MissingFieldError,
which should reduce confusion.
@benjamn
Copy link
Member

benjamn commented May 29, 2020

Shortly after the AC3.0 release, we are hoping to introduce some sort of field policy configuration to specify what happens when references contained by a field no longer point anywhere.

For example, the policy could control whether the field itself gets removed, or what happens to lists that contain dangling references. I think the default behavior will continue to be leaving the dangling references untouched, but you could opt into fancier behavior where it's useful.

It's definitely tempting to do this work as part of cache.gc, since garbage collection already has to examine all the reachable references in the cache, and it's reasonable for cache.gc to follow cache.evict.

Note: with #6365, I am hoping to establish a policy that dangling references behave as much as possible like references to empty objects, so you won't see these Dangling reference ... errors anymore, but just the normal Can't find field 'name' on … errors. I'd be happy to hear any thoughts/experiences you have regarding that policy.

benjamn added a commit that referenced this issue May 30, 2020
)

Question: When cache.read{Query,Fragment} encounters a dangling reference
(one that does not currently refer to any normalized entity object in the
cache), should the cache behave as if the (nonexistent) target of the
reference was an empty object, and report any requested fields as missing,
or should the cache generate some other kind of error that reflects the
the absence of the whole object?

I think we could answer this question either way, but I'm leaning towards
the first option.

Note: it's normal for the cache to end up with dangling references when
whole entity objects are evicted, or when a reference is created (e.g. by
toReference) without writing any data into the cache. Cleaning up dangling
references is a tricky problem, requiring application-level reasoning, and
not even always desirable, since the data could always come back into the
cache later, restoring the validity of the reference.

Previously, I thought it would be helpful to distinguish between the
absence of an entity object and the object simply being empty, but I no
longer think this distinction (which only affected the wording of the
MissingFieldError description) matters very much, and we can simplify
everything by adopting the following policy:

> During cache reads, a dangling Reference should behave as much as
  possible like a Reference to an entity object that happens to contain
  zero fields.

I'm optimistic this policy may help with issues like #6325. At the very
least, this policy means there's now only one flavor of MissingFieldError,
which should reduce confusion.
@jebonfig
Copy link

Note: with #6365, I am hoping to establish a policy that dangling references behave as much as possible like references to empty objects, so you won't see these Dangling reference ... errors anymore, but just the normal Can't find field 'name' on … errors. I'd be happy to hear any thoughts/experiences you have regarding that policy.

@benjamn So with these changes a cache-only query now returns an empty object {} on a cache miss? Did you all consider returning null or undefined instead of the empty object? Feels like that would be easier to handle.

A simple example being a CarsQuery using cache-and-network which fetches the entire list of cars, and a CarQuery using cache-only which gets a single car by it's id (via Query typePolicy). On page load, the CarQuery component will render before the component with CarsQuery has finished loading from the server, but once it does load, the CarQuery will automatically pick up the individual item from the cache. In code that would look like:

const { 
    data: {
        car
    } = { car: null }
} = useQuery<CarData, CarVars>(CarQuery, {
    variables: {
        id: car_id!
    },
    fetchPolicy: 'cache-only'
});

if (car) {
    // render car details markup
}

//versus a more verbose check of checking for an empty object

if (car && Object.keys(car).length === 0) {
    // render car details markup    
}

@jitensuthar
Copy link

jitensuthar commented Jun 4, 2020

Note: with #6365, I am hoping to establish a policy that dangling references behave as much as possible like references to empty objects, so you won't see these Dangling reference ... errors anymore, but just the normal Can't find field 'name' on … errors. I'd be happy to hear any thoughts/experiences you have regarding that policy.

@benjamn So with these changes a cache-only query now returns an empty object {} on a cache miss? Did you all consider returning null or undefined instead of the empty object? Feels like that would be easier to handle.

A simple example being a CarsQuery using cache-and-network which fetches the entire list of cars, and a CarQuery using cache-only which gets a single car by it's id (via Query typePolicy). On page load, the CarQuery component will render before the component with CarsQuery has finished loading from the server, but once it does load, the CarQuery will automatically pick up the individual item from the cache. In code that would look like:

@benjamn Haivng policies to affect how dangling references are handled sounds promising (and including it also as part of GC makes sense to me).

I agree with the above though in preferring cache misses come back with null or undefined fields instead of empty objects (also in returnPartialData cases) as the default behavior. It's a lot less awkward and more natural to null-test vs. testing keys to see if a field or response contains an empty object. Curious about arguments in favor of empty objects over null or undefined.

@dodas
Copy link

dodas commented Jun 4, 2020

I agree with the above though in preferring cache misses come back with null or undefined fields instead of empty objects (also in returnPartialData cases) as the default behavior. It's a lot less awkward and more natural to null-test vs. testing keys to see if a field or response contains an empty object. Curious about arguments in favor of empty objects over null or undefined.

I think this change has been reverted and dangling references are treated the same way they were before - throwing error.
See 5e74226.

@jitensuthar
Copy link

I agree with the above though in preferring cache misses come back with null or undefined fields instead of empty objects (also in returnPartialData cases) as the default behavior. It's a lot less awkward and more natural to null-test vs. testing keys to see if a field or response contains an empty object. Curious about arguments in favor of empty objects over null or undefined.

I think this change has been reverted and dangling references are treated the same way they were before - throwing error.
See 5e74226.

I'm still getting back an empty object when I have returnPartialData flag set on a useQuery hook where one of the fields holds a dangling reference (resulting from an earlier cache eviction).

I was only null-testing that particular field so it fell through the cracks, which led me here. I am proposing returning null or undefined on fields that hold dangling references in a partial cache result as well (this is as of 3.0.0-rc.0)

@benjamn
Copy link
Member

benjamn commented Jul 10, 2020

Quick updates: Yes, #6365 was reverted—sorry for that distraction. However, please have a look at #6454 (which implements #6425 (comment)) for a more automatic (and I think pretty elegant!) solution to the problem of dangling references in lists. Avoiding manual updates after eviction is an area of ongoing investigation and improvement, but I believe/hope these changes make a meaningful difference.

@benjamn
Copy link
Member

benjamn commented Jul 10, 2020

Closing because the original example provided by @danReynolds should now work without any manual effort (the list is automatically filtered using the canRead helper, so there's no missing field error). Please open a new issue if you've got a different example you'd like us to investigate!

@benjamn benjamn closed this as completed Jul 10, 2020
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 16, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants