-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Need write back cache #1622
Comments
I'm pretty sure this is 100% impossible short of massive code duplication from kube-apiserver. We can't know what kind of transforms would happen on save (webhooks, core types with funky storage adapters, etc) and making this work for Patch requests would be extra super fun. At best we could write back to the cache with the returned object data after the update call but that's different than write-through. |
Thanks for the prompt reply Noah @coderanger. Maybe write-through isn't the exactly the term then :) Shall I replace 'write-through' with 'write-back' and update description above? |
Re-worded things based on coderanger's comments. |
Is there a reason to not use the substantially simpler solution of "use an uncached client for this particular point in the code"? |
I considered that but looking at |
Yes? I mean this is one of the major downsides to using the objects themselves for stateful storage as you are in Crossplane. My usual recommendation is either use a configmap/secret or a dedicate state storage type. And in general Crossplane is trying to bridge the gap between a convergent-only system (k8s) and a "we've heard about convergence but not totally on board" system (cloud APIs). You're going to have some cases where caching is going to be unhelpful because you need external state storage because the cloud provider's API is neither convergent nor stateful. |
So what is the downside up updating the cache with the response from the server? Or at least making it easy for someone to opt-in for that behavior? |
It's very fiddly :) It means there's many places that cache updates can come from as opposed to just one, watch push events. |
There could be a boolean option to specify whether write back cache happens at all with a default to 'no'. |
That still adds substantial complexity to the code. And that's generally a terrible design style. If you are worried about something being a problem, you don't fix the problem by making only a few people experience it. |
While I agree that the proposed change makes the code more complex I wouldn't characterize as substantially more complex. And looking through I see there are at least a few others who have opened issues because they didn't expect the current behaviour. ( #1543, #1464, #1349 ). And there is this trickiness to try to help avoid use of a stale cache here
So at least for some the current behaviour doesn't seem to adhere to the principle of least astonishment. That's not so great either. Btw I don't understand what you meant by 'If you are worried about something being a problem, you don't fix the problem by making only a few people experience it.'. Particularly given that I see delegatingReader has options to disable caching for unstructured data and for specified gvk. Would you please elaborate? |
I don't think manipulating the cached data is an option, because the data in the shared informer is managed on the basic assumption that there is a resource version and the data we have reflects the data at that resource version in the apiserver. Breaking this assumption has way too much potential to introduce issues and I would be very surprised if upstream accepts submissions into that direction. A less invasive approach would be to wrap the client with something that blocks after mutating calls until it finds the response it got from the api in the cache. The main drawback is that it will introduce quite a bit of latency into mutating calls. |
@alvaroaleman If the process were to be
The change would just expedite bringing a just written change into the cache. |
A very basic example on how things go wrong with a writeback cache just off of the top of my head and I am very sure there are more:
No, because there are also RVs for lists. If we mess with the cache, the reflector will think we are at a given RV when we are not, because we manually manipulated the data in the store. |
What if this write back behavior was limited to only those objects a generation value? ( https://github.com/zecke/Kubernetes/blob/master/docs/devel/api-conventions.md ) If nothing ever updated the cache with an object whose generation value was less than the value for whatever was in the cache already there would be a risk of pushing stale data back into the cache. |
I like the sound of this approach. I personally (as a representative of the Crossplane project) am aligned with the controller-runtime maintainers here and feel good about coding for the assumption that cache reads may be stale. It would definitely be ideal to make that contract obvious to controller authors though, as it did surprise me at first. A little more context on the original issue referenced by this one (https://github.com/crossplane/crossplane/issues/2435). Many of our Crossplane controllers orchestrate external APIs - i.e. cloud providers etc - rather than Kubernetes APIs. A subset of those APIs (frustratingly) return non-deterministic unique identifiers at create time, so sometimes when we call an API to create a thing it's critical that we persist the ID of that thing. If we don't, we'll assume we need to call create again on the next reconcile. This means for us a stale cache read can potentially result in us creating something twice (because the stale read may not contain the ID we recorded). We opted to alleviate this issue by calling |
Orthogonal to this issue here, but a better approach for this for many resources at many cloudprovides is to tag them during creation and then use a filtered list request to check if it already exists. This way you don't need to persist and it also allows you to deal with the occasional "Create request failed but creation actually succeeded".
Lists don't have a generation. |
I don't necessarily disagree, but "better" is a bit subjective here. When you're dealing with hundreds of different API implementations across tens of different external systems (different clouds, etc) there's not always a reliable and consistent metadata API to use. :) |
If the cache understands It would solve the "I can do a PUT followed immediately by a GET and not see what I just PUT" situation though, which is the most surprising bit to me. As @dee0sap highlighted, it's quite common in controllers to perform incremental updates and that pattern seem to interact poorly with the reality that your subsequent reconcile can end up operating on data without the previous change that your own process made. Even if it doesn't cause any problems per-se it's still awkward. It's also weird from a data movement perspective IMO. The fact that the server bothers to reply to a PUT showing me an updated resource, with an updated |
No, refer to https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions, specifically this:
|
The Kubernetes project currently lacks enough contributors to adequately respond to all issues and PRs. This bot triages issues and PRs according to the following rules:
You can:
Please send feedback to sig-contributor-experience at kubernetes/community. /lifecycle stale |
The Kubernetes project currently lacks enough active contributors to adequately respond to all issues and PRs. This bot triages issues and PRs according to the following rules:
You can:
Please send feedback to sig-contributor-experience at kubernetes/community. /lifecycle rotten |
The Kubernetes project currently lacks enough active contributors to adequately respond to all issues and PRs. This bot triages issues and PRs according to the following rules:
You can:
Please send feedback to sig-contributor-experience at kubernetes/community. /close |
@k8s-triage-robot: Closing this issue. In response to this:
Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. |
Ticket https://github.com/crossplane/crossplane/issues/2435 was opened a due to a leak of resources in AWS.
Multiple reasons for the leak were found. One of the reasons is a caching/concurrency problem in controller-runtime.
In the above ticket the caching/concurrency problem involved an AWS route table managed resource. Reconcile created an additional one in AWS because after it had set the external name it was asked to Reconcile again when the cache had an out of date instance of the route table, one where external-name hasn't been set yet. This happened despite the use of HasSynced in specificInformersMap::Get.
These comments to the ticket give the details of how this is happening
https://github.com/crossplane/crossplane/issues/2435#issuecomment-891318989
https://github.com/crossplane/crossplane/issues/2435#issuecomment-890462636
https://github.com/crossplane/crossplane/issues/2435#issuecomment-890424587
A couple of 'band-aid' fixes would be to
The first would only solve the problem for AWS route tables which isn't ideal.
The second would only solve the problem when the additional Reconcile, the one where an additional resource in AWS is created, is the result of reference resolution. However since updates to the managed resource object in the API server could come from anywhere this also isn't ideal.
A write-back cache would provide a complete solution. Here is an outline of the changes I think are required.
== client-go ==
These changes just make it so cache mutation continues to work as expected.
shared_informer.go
addindexer
which embedsIndexer
.Move
cacheMutationDetector
fromsharedIndexInformer
toindexer
.indexer
will wrap calls toAdd
,Update
andReplace
. The wrapper methods will forward the passed in object tocacheMutationDetector
before the object to the appropriate method of the embeddedIndexer
.NOTE: This plugs a gap in the current cache mutation detection that exists today. Today detection only is performed on objects added to the cache via
sharedIndexInformer::HandleDeltas
.shared_informer.go
removecacheMutationDetector
fromsharedIndexInformer
. Change construction ofsharedIndexInformer
to useindexer
mentioned above.== controller-runtime ==
CacheUpdater
which is similar toCacheReader
defined incache_reader.go
.It only has an
Update
method which forwards its parameters toindexer.Update
.Updater
method toMapEntry
.addInformerToMap
in so it setsUpdater
ofMapEntry
.Updater
interface tointerfaces.go
. This just hasUpdate
method identical to the one on theWriter
interface.Update
method toinformerCache
. which will be similarGet
etc.Cache
in cache.go so it embeds aboveUpdater
.cacheWriteBAck
struct similar todelegatingReader
. It will embed Writer and it will have to the cache and theuncachedGVKs
. It will forward calls toWriter
first and if those succeed and, ifuncachedGVKs
doesn't indicate otherwise, callUpdate
on thecache
, passing the updated object from the server.NewDelegatingClient
so that constructs adelegatingClient
with acacheWriteThrough
for the writer.@hasheddan @negz
The text was updated successfully, but these errors were encountered: