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

Response Cache should allow cache invalidation based on Operation typename and input variables #3125

Open
gthau opened this issue Dec 1, 2023 · 3 comments
Assignees

Comments

@gthau
Copy link
Contributor

gthau commented Dec 1, 2023

Is your feature request related to a problem? Please describe.

I'm trying to use Yoga Response Cache plugin in my API. However I'm facing an issue because I need to constantly invalidate most of the cache, rather than invalidating only what I'd like to.
The limiting factor is mostly when a mutation creates a new entity rather than modifies or deletes an entity. In such cases, I don't find a way to invalidate "related" operations cached that might be affected.

So far, I think this can be generalized to the following statement:

when there are cached responses for collections of Entities based on different criteria, and a new Entity is created, only some of the cached responses should be invalidated, but currently all cached responses for the Entity typename have to be invalidated.

Description of a simplified use case
Let's consider a User type:

type User {
  id: Int!
  role: String!
}

and some queries that fetch those by range (e.g. getUsersInRange) or by prop (e.g. getUsersByRole).

type Query {
  getUsersInRange(from: Int!, to: Int!): [User!]!
  getUsersByRole(role: String!): [User!]!
}

Case1: a mutation creates an Entity
When a mutation create a new User, I can't invalidate the cached response for getUsersByRange or getUsersByRole in a granular fashion, I can only invalidate for the typename User, leading to all cached responses having entities of type User being invalidated.

Case2: a mutation updates an Entity
if a mutation changes some Entity prop, leading to the need to invalidate the collection of Entities cached. E.g. a mutation changes User { id: 1, role: 'admin' } to { id: 1, role: 'superuser'}. The cache invalidation:

  • will work out of the box for the cached response of query getUsersByRole with input role: 'admin' because this cached response has collected the entity User { id: 1 }
  • however it will not work for the cached response of query getUsersByRole with input role: 'superuser' because this cached response had not collected the entity User { id: 1 }.

Describe the solution you'd like

I'd like to have better granular control over the invalidation, based on the query typename and the operation variables so that I can handle use Case 1 and 2.

Case 1:
E.g. I create a User { id: 25, role: 'superuser' }, I want to be able to invalidate:

  • cached response for the query getUsersByRole where input role was 'superuser' (I know the same query with input role = 'admin' or role = 'user' are not affected) and
  • cached responses of getUsersInRange where the id of the created user (newId) is included in range [from, to] of this operation input variables (I know cached responses for getUsersInRange with [from, to] doesn't include newId can be maintained in the cache).

Case 2:
E.g I update User { id: 1, role: 'admin' } to { id: 1, role: 'superuser'}, I want to be able to invalidate:

  • the cached response of getUsersByRole where input role was 'superuser'
  • the cached response of getUsersInRange where input is [from=20, to=29] (I know the other ranges are not affected)

Describe alternatives you've considered

Use a dataloader for queries that returns collections of Entities based on some criteria. With a provided cache that I have control over, I can call the invalidate method for a precise key and/or loop over the cached items, parse the key and decide to invalidate or not.

This workaround is not so great because all resolvers have to be executed, which can be very costly for collections of Entities. The Response Cache is more efficient as it completely bypass the resolvers once a response is cached.

Additional context

Reproduction scenario: https://codesandbox.io/p/devbox/shy-dream-vtlsz8

@EmrysMyrddin
Copy link
Collaborator

Hi, thank you for your proposal, this is very interesting!

We are currently thinking about how to make an API for this, the related issue can be found on the Envelop plugin repo, since it is not specific to Yoga: n1ru4l/envelop#1979

The main thing that I'm not sure about this approach is that it would create very strong bonds between resolvers of the schema, making the whole server rigid and error prone to add/remove resolvers.

Imagine you already have a complete schema with careful fine grained cache control. If you now add a new root query, it means you will have to carefully think about every mutations or places in code that could possibly make this new resolver cached response stale, and update them. Same thing if you delete an existing root query.

I would prefer a more global and approach, with as less configuration as possible. That would lead to a more predictable cache behaviour.

@klippx
Copy link
Contributor

klippx commented Dec 8, 2023

I think one option would be if you can keep the core response cache simple and invert the responsibility to the userland.

In our current implementation (apollo response cache) we are using the equivalent of buildResponseCacheKey to construct custom cache keys. Basically, we filter through the relevant input variables on key/value form, and serialize an array such as
relevantInputVars.map((key, value) => key + ':' + value}) which gives a result of e.g. ["market:GB", "userType:admin"] which we then join together and append to the cache key.

In our case, we have flushCache mutations that can then flush all GB keys for instance. Something similar can be used here, if a mutation updated GB market we can invalidate only the GB part of the cache. Today in Yoga there is something called invalidateViaMutation but we would need something more granular: It could be a callback instead which yields relevant info such as contextValue, query, input variables, etc. And then it can return a pattern (or patterns) to flush, which Yoga Response Cache has to relay to the Cache somehow, perhaps via invalidate method with all relevant patterns to flush.

So the suggestion is to do as little as possible in the framework and invert to the user to define how to build keys and how to invalidate them, somehow. No idea how, just throwing some idea and see if someone can get a more refined idea :)

@EmrysMyrddin
Copy link
Collaborator

That's a good idea, this is probably something you can actually do in a seperate custom plugin (which takes the same cache instance than the useResponseCache plugin). Perhaps we can explore this and add some documentation to guide people into implementing this kind of advanced use cases.

I like this approach because we are in a level of abstraction that begins to be too business dependent to provide a good default behaviour in the framework that would work for the majority of cases. Delegating this kind of advances use cases to end users (or even to more specialised sidecar plugins ?) is probably a good idea

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants