apollographql / apollo-kotlin

:rocket:  A strongly-typed, caching GraphQL client for the JVM, Android, and Kotlin multiplatform.
https://www.apollographql.com/docs/kotlin
MIT License
3.73k stars 654 forks source link

handle subgraph/field expiration/timeouts #3709

Open kenyee opened 2 years ago

kenyee commented 2 years ago

Use case In case a graphql graph has mixed expiration timeouts in subgraphs or fields, the Apollo graphql client doesn't have an API to handle these cases. E.g., a simple use case would be a product site like Wayfair or Amazon....price fields are only valid for 10min, but product descriptions might be valid for a much longer time (e.g. 1 day). Apollo federation server does set the query result to min(subgraph[].timeout), so one workaround is to break this into two different queries. Apps can also use the executionContext to get the expiration timeout from the http expires header to know when to do another request to updated data.

Describe the solution you'd like Ideally, the federation server would also send down expiration info for subgraphs and the graphql client would know to request only subgraphs. If the streaming API is used, as long as there are subscribers, there should be a flow of updates for that data. and maybe include a staleness parameter: apolloClient.query(query).httpFetchPolicy(CacheFirst).acceptStaleDataFor(60.min).execute()

martinbonnin commented 2 years ago

Thanks for writing this! I think this boils down to:

  1. There is currently no "easy" way to expose per-field expiration information in a response. So it's either: a. Exposing a global "maxAge" for the whole response. This is the easiest and what @cacheControl is currently doing. This mean the minimum maxAge is going to be used accross the whole query and some fields will expire early. b. Adding the "maxAge" in the schema itself. This is the most flexible but if done for every field, it will greatly increase the size of the response. Also there is no standard way to do this so it's up to the caller to handle this.
  2. There is currently no "easy" way to expose per-field expiration information in the Apollo Kotlin cache. The current options are: a. HttpCache has "CACHE_EXPIRE_TIMEOUT_HEADER" which is client controlled and doesn't read the server value a. MemoryCache has "expireAfterMillis" which is client controlled and doesn't read the server value b. SqlNormalizedCache has no expiration

All in all, We'll work about 2. I opened https://github.com/apollographql/apollo-kotlin/issues/3566 a few days ago. I don't think we can do much about 1.? If the information is not in the response, there's not much we can do.

A few other comments:

Ideally, the federation server would also send down expiration info for subgraphs and the graphql client would know to request only subgraphs.

I don't think the client should know anything about federation and subgraphs? That's the nice thing with GraphQL is that client developers don't need to know anything about how it's implemented server-side. There's still the question about per-field expiration information (1. above).

apolloClient.query(query).httpFetchPolicy(CacheFirst).acceptStaleDataFor(60.min).execute()

Good point about accepting stale data. I've updated https://github.com/apollographql/apollo-kotlin/issues/3566 with some more information.

I'm keeping this issue open as a global server + client brainstorming issue but for all client APIs and discussion, please go to https://github.com/apollographql/apollo-kotlin/issues/3566 instead

kenyee commented 2 years ago

I don't think the client should know anything about federation and subgraphs? That's the nice thing with GraphQL is that client developers don't need to know anything about how it's implemented server-side

That was mainly in case app developers want to implement their own client side caching of a subgraph if they don't want to use Apollo client SDK caching. That expires info can come down as part of the data in a separate field as you mentioned though.

mindwalkr commented 2 years ago

We don't need TTL info to be pulled down with every data query. The TTL information should be as static as the graph itself. eg: a Product Price should be a Price .... if the implementer wants the same graph node with different TTL's then maybe they should rethink what they are doing and implement it as different nodes.

How about a solution where the client (mobile app or SPA) pulls down a separate graph which defines the TTL for each node on the graph?

At least for mobile app, it could only pull down the TTL graph once and then push it to the Apollo client. I envisage a mobile App starts with a cached copy and does an async pull of the "latest" TTL graph and then updates the Apollo client with the "latest" version of the TTL graph.

martinbonnin commented 2 years ago

@mindwalkr gotcha. I've added a "client-side expiration logic" section to https://github.com/apollographql/apollo-kotlin/issues/3566. There's significant work involved. The biggest challenge certainly being able to store the per-field expiration metadata without doubling (or worse) the size of the cache...