-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Improve defaultMaxAge setting #4162
Conversation
@stevez86: Thank you for submitting a pull request! Before we can merge it, you'll need to sign the Apollo Contributor License Agreement here: https://contribute.apollographql.com/ |
So, the way that maxAge is documented is:
If I understand correctly, you are not reporting a bug in the implementation where it does not match the docs, but you're saying that the documented and implemented behavior is not useful for you. Is that correct? I definitely agree that these are very surprising semantics; even when I personally implemented one generation of our caching support I found it odd. But I also think that just changing the default semantics of the cache control annotations incompatibly is not advisable. Fundamentally, the problem is that it's not knowable whether your The choice made by the Apollo cache-control spec is to err on the side of under-caching by assuming any object-typed field is likely to be a separate lookup by default (but that scalars are likely to be "part of" their parent types). My understanding is that this PR lets you write Rather than just change the default in such a blunt fashion, it seems to me it would be better to extend the spec to make the actual semantic concept more explicit. ie, something like My mind is far enough from the details of how this works today that I'm not sure if Does that make sense? |
That is correct. While confusing, it's not a bug per the documentation. It's an enhancement in functionality and is closer for me to the intuitive functionality that I assumed when initially implementing caching. I only recently after a couple years fully understood what it was doing because of this PR.
I agree with that approach but the way its implemented for a fairly common use case actually leads to a less conservative development environment that I believe is contrary to the goal of the Apollo team. The problem with the current implementation is that as soon as I say I want to cache (by setting cacheControl to an object or true) I am then forced to use a defaultMaxAge which by default is 0 - there is no opt-out. Now take my example above, which has two resolvers returning an object that needs to be cached in one instance and definitely not cached in another. The only way (that I'm familiar with) to implement this use case in the current implementation is to set a defaultMaxAge to my highest cache setting and then set the cache for the user resolver to 0. What this means is that I have to default to an unsafe (high cache) setting for my entire schema and manually make sure all protected fields are set to a cache age of 0 - definitely contrary to the methodology that you mentioned Apollo was taking.
Completely agree and I'd be happy to work towards that, but I think there is enough confusion about what Take a look at the code change (its basically 2 simple lines + tests). It doesn't break existing functionality and is opt-in. If you don't opt-in, the current implementation remains the same. If you opt-in no cache headers are set without the developer setting them. And it solves the problems and confusion from here: #3559 |
So I don't think your fix will work well, because not only does it ignore object fields nested inside cachecontrolled fields, it also ignores root fields. So eg, with
Given the query You could make the conditional more complex to explicitly treat root fields differently than nested object/interface fields. But I do think that making the concept of inheritance explicit is the way to go. While I think the original design of "assume every sub-object might be a separate database call" was overly conservative, "fixing" this by only providing the option of "assume every sub-object was fetched as part of its parent" seems far too blunt. |
I see, I didn't think of that and agree that a more explicit inheritance is a good way to go. Thank you for your feedback and time looking at this. |
Previously, the cache control logic treats root fields and fields that return object or interface types which don't declare `maxAge` specially: they are treated as uncachable (`maxAge` 0) by default. You can change that 0 to a different number with the `defaultMaxAge` option, but you can't just make them work like scalars and not affect the cache policy at all. This PR introduces a new argument to the directive: `@cacheControl(noDefaultMaxAge: true)`. If this is specified on a root or object-returning or interface-returning field that does not specify its `maxAge` in some other way (on the return value's type or via `setCacheHint`), then the field is just ignored for the sake of calculating cache policy, instead of defaulting to `defaultMaxAge`. Note that scalar fields all of whose ancestors have no `maxAge` due to this feature are now treated similarly to scalar root fields. Also change `info.cacheControl.cacheHint` to update when `setCacheHint` is called. One use case for this could be in federation: `buildFederatedSchema` could add this directive to all `@external` fields. This addresses concerns from #4162 and #3559.
Here's an implementation of something that's similar to the |
Previously, the cache control logic treats root fields and fields that return object or interface types which don't declare `maxAge` specially: they are treated as uncachable (`maxAge` 0) by default. You can change that 0 to a different number with the `defaultMaxAge` option, but you can't just make them work like scalars and not affect the cache policy at all. This PR introduces a new argument to the directive: `@cacheControl(noDefaultMaxAge: true)`. If this is specified on a root or object-returning or interface-returning field that does not specify its `maxAge` in some other way (on the return value's type or via `setCacheHint`), then the field is just ignored for the sake of calculating cache policy, instead of defaulting to `defaultMaxAge`. Note that scalar fields all of whose ancestors have no `maxAge` due to this feature are now treated similarly to scalar root fields. Also change `info.cacheControl.cacheHint` to update when `setCacheHint` is called. One use case for this could be in federation: `buildFederatedSchema` could add this directive to all `@external` fields. This addresses concerns from #4162 and #3559.
Previously, the cache control logic treats root fields and fields that return object or interface types which don't declare `maxAge` specially: they are treated as uncachable (`maxAge` 0) by default. You can change that 0 to a different number with the `defaultMaxAge` option, but you can't just make them work like scalars and not affect the cache policy at all. This PR introduces a new argument to the directive: `@cacheControl(noDefaultMaxAge: true)`. If this is specified on a root or object-returning or interface-returning field that does not specify its `maxAge` in some other way (on the return value's type or via `setCacheHint`), then the field is just ignored for the sake of calculating cache policy, instead of defaulting to `defaultMaxAge`. Note that scalar fields all of whose ancestors have no `maxAge` due to this feature are now treated similarly to scalar root fields. One use case for this could be in federation: `buildFederatedSchema` could add this directive to all `@external` fields. This addresses concerns from #4162 and #3559.
Previously, the cache control logic treats root fields and fields that return object or interface types which don't declare `maxAge` specially: they are treated as uncachable (`maxAge` 0) by default. You can change that 0 to a different number with the `defaultMaxAge` option, but you can't just make them work like scalars and not affect the cache policy at all. This PR introduces a new argument to the directive: `@cacheControl(noDefaultMaxAge: true)`. If this is specified on a root or object-returning or interface-returning field that does not specify its `maxAge` in some other way (on the return value's type or via `setCacheHint`), then the field is just ignored for the sake of calculating cache policy, instead of defaulting to `defaultMaxAge`. Note that scalar fields all of whose ancestors have no `maxAge` due to this feature are now treated similarly to scalar root fields. One use case for this could be in federation: `buildFederatedSchema` could add this directive to all `@external` fields. This addresses concerns from #4162 and #3559.
Previously, the cache control logic treats root fields and fields that return object or interface types which don't declare `maxAge` specially: they are treated as uncachable (`maxAge` 0) by default. You can change that 0 to a different number with the `defaultMaxAge` option, but you can't just make them work like scalars and not affect the cache policy at all. This PR introduces a new argument to the directive: `@cacheControl(noDefaultMaxAge: true)`. If this is specified on a root or object-returning or interface-returning field that does not specify its `maxAge` in some other way (on the return value's type or via `setCacheHint`), then the field is just ignored for the sake of calculating cache policy, instead of defaulting to `defaultMaxAge`. Note that scalar fields all of whose ancestors have no `maxAge` due to this feature are now treated similarly to scalar root fields. One use case for this could be in federation: `buildFederatedSchema` could add this directive to all `@external` fields. This addresses concerns from #4162 and #3559.
Previously, the cache control logic treats root fields and fields that return object or interface types which don't declare `maxAge` specially: they are treated as uncachable (`maxAge` 0) by default. You can change that 0 to a different number with the `defaultMaxAge` option, but you can't just make them work like scalars and not affect the cache policy at all. This PR introduces a new argument to the directive: `@cacheControl(noDefaultMaxAge: true)`. If this is specified on a root or object-returning or interface-returning field that does not specify its `maxAge` in some other way (on the return value's type or via `setCacheHint`), then the field is just ignored for the sake of calculating cache policy, instead of defaulting to `defaultMaxAge`. Note that scalar fields all of whose ancestors have no `maxAge` due to this feature are now treated similarly to scalar root fields. One use case for this could be in federation: `buildFederatedSchema` could add this directive to all `@external` fields. This addresses concerns from #4162 and #3559.
Previously, the cache control logic treats root fields and fields that return object or interface types which don't declare `maxAge` specially: they are treated as uncachable (`maxAge` 0) by default. You can change that 0 to a different number with the `defaultMaxAge` option, but you can't just make them work like scalars and not affect the cache policy at all. This PR introduces a new argument to the directive: `@cacheControl(inheritMaxAge: true)`. If this is specified on an field returning a composite type (object, interface, or union) or on the composite type itself, and it does not specify its `maxAge` in some other way (on the return value's type or via `setCacheHint`), then the field is just ignored for the sake of calculating cache policy, instead of defaulting to `defaultMaxAge`. Note that this does *not* affect root fields (scalar or composite). You still need to make sure that every root field is declared as cachable to have a cachable operation. This just lets you say that a nested composite form is "part of" its parent for the purposes of caching, just like nested scalar fields are by default. The behavior described above (and looking on the field's return type for `@cacheControl` in the first place) previously applied to fields returning object and interface types (possibly nested in some layers of not-null and/or list-of). This PR makes things more consistent by treating the third composite type (unions) in the same way. One use case for this could be in federation: `buildFederatedSchema` could add this directive to all `@external` fields, since their values are typically provided directly in the arguments to the `Query._entities` field. This addresses concerns from #4162 and #3559.
This addresses the following issue: #3559 - where a defaultMaxAge is required and there is no option to not have the blanket defaultMaxAge functionality.
If you elect not to have a
defaultMaxAge
of 0 set for all resolvers and types you must explicitly set it tonull
. For example:This is useful if you have multiple queries of identical types like in the example shown,
query user
will have amaxAge
of0
whilequery userFoo
will have amaxAge
of100
which would not be possible under the current usage ofdefaultMaxAge
because you can't set the child to have two different maxAges depending on parent.