feat: client cache for configurable list of gvks#1042
Conversation
|
Warning Review limit reached
Next review available in: 59 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (12)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Test Coverage ReportTest Coverage 📊: 70.3% |
mblos
left a comment
There was a problem hiding this comment.
Very nice work :) some first comments (still not completed with reviewing)
| // overlayList merges the overlay entries for the GVK into the informer result, | ||
| // deduplicating by objectKey (overlay wins), dropping tombstones, and filtering | ||
| // overlay-only entries against the list options' label and field selectors. | ||
| func (o *overlay) overlayList(gvk schema.GroupVersionKind, existing []runtime.Object, lo *client.ListOptions) []runtime.Object { |
There was a problem hiding this comment.
If a write changes an indexed field, the inner List filters against the informer (old value), then overlayList silently replaces the object with the new version.
Example: informer has spec.hostName=host-A (RV=5), overlay has spec.hostName=host-B (RV=6). List(MatchingFields{"spec.hostName": "host-A"}) includes the object from the informer, then overlayList swaps in the overlay version.
So you get a host-B object in a host-A query.
Or am I overlooking something?
|
|
||
| // evictionHandler returns an informer event handler that evicts overlay entries | ||
| // for the GVK when the real object is observed at a >= ResourceVersion. | ||
| func (c *CachingClient) evictionHandler(gvk schema.GroupVersionKind) toolscachek8s.ResourceEventHandler { |
There was a problem hiding this comment.
is DeleteFunc missing here? what if eg someone via kubectl deletes the crd, the informer fires a delete .. but the overlay does not react on it, or?
|
|
||
| // Delete delegates to the inner client and, on success for a cached GVK, stores | ||
| // a tombstone in the overlay. | ||
| func (c *CachingClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error { |
| } | ||
|
|
||
| func keyForObject(obj client.Object) objectKey { | ||
| return objectKey{namespace: obj.GetNamespace(), name: obj.GetName()} |
There was a problem hiding this comment.
What if a crd resource has the same name across lh clusters?
PhilippMatthes
left a comment
There was a problem hiding this comment.
Thank you for incorporating my feedback from #1015 -- especially, the informer-cache idea and reusing metav1.Duration! I've copied over some thoughts and questions and had some new ones along the way. Thanks for considering my feedback.
| hypervisorOvercommitController.Client = multiclusterClient | ||
| if err := hypervisorOvercommitController.SetupWithManager(mgr); err != nil { | ||
| hypervisorOvercommitController.Client = cachingClient | ||
| if err := hypervisorOvercommitController.SetupWithManager(mgr, multiclusterClient); err != nil { |
There was a problem hiding this comment.
Can this cause issues? Inside the SetupWithManager function, aren't there calls that would need to be tunneled by the caching client, such as defining indexes or resource handlers?
Is it possible to wrap it the other way around? Instead of ctrl.Client -> mcl.Client -> clientcache.Client, do ctrl.Client -> clientcache.Client -> mcl.Client? This wouldn't break the pattern here
| clientcache: | ||
| gvks: | ||
| - cortex.cloud/v1alpha1/Reservation | ||
| keystoneSecretRef: |
There was a problem hiding this comment.
Is it expensive to cache resources? If not, why not cache all resources w/o extra configuration?
|
|
||
| // Inner returns the wrapped client, e.g. for use with a controller Builder that | ||
| // needs the raw client rather than the caching wrapper. | ||
| func (c *CachingClient) Inner() client.Client { return c.Client } |
There was a problem hiding this comment.
CachingClient is not an interface. What purpose does this method have?
| func (c *CachingClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { | ||
| if err := c.Client.Update(ctx, obj, opts...); err != nil { | ||
| return err | ||
| } | ||
| if gvk, cached := c.gvkFor(obj); cached { | ||
| c.overlay.upsert(gvk, obj) | ||
| } | ||
| return nil | ||
| } |
There was a problem hiding this comment.
What happens when there are two concurrent calls to this function?
Can there be a race condition w.r.t the client update executing in another order than the cache update? Ideally, we should add a globally shared mutex across all CRUD functions so that concurrently reading/writing routines see a consistent state.
| // overlay is the generic, client-independent core of the cache. It stores | ||
| // pending writes keyed by GVK and objectKey and merges them into informer | ||
| // read results until the real object is observed in an informer (eviction). | ||
| type overlay struct { |
There was a problem hiding this comment.
| type overlay struct { | |
| type cache struct { |
| return err | ||
| } | ||
| if gvk, cached := c.gvkFor(obj); cached { | ||
| c.overlay.upsert(gvk, obj) |
There was a problem hiding this comment.
Is it intended that this method call cannot fail when the expected resource has not been created yet? Doesn't this violate the expected controller-runtime client interface protocol?
| type CachingClient struct { | ||
| client.Client // inner client, used for delegation | ||
|
|
||
| informers InformerSource | ||
| scheme *runtime.Scheme | ||
| overlay *overlay | ||
| ttl time.Duration | ||
| gvks map[schema.GroupVersionKind]bool | ||
| } |
There was a problem hiding this comment.
I would prefer if we flatten out the caching logic into the client, and not have a separate cache struct.
This would make it easier to understand where the resources stored in the map locally deviate from the resources stored in the controller-runtime client.
For example, the Delete method won't delete the resource from the map immediately -- it will set a deleted flag on the resource. What happens from there, no clue.
| // used for eviction, scheme resolves object GVKs, and conf lists the GVKs to | ||
| // overlay and the TTL. GVK strings are formatted as "<group>/<version>/<Kind>" | ||
| // and are resolved against scheme. | ||
| func New(inner client.Client, informers InformerSource, scheme *runtime.Scheme, conf Config) (*CachingClient, error) { |
There was a problem hiding this comment.
If I see it correctly, you are inputting New(mcl.Client, mcl.Client) into this function in main.go -- what's the purpose of mapping out the InformerSource as an additional interface instead of just taking client.Client?
| return nil, fmt.Errorf("clientcache: object for gvk %s does not implement client.Object", gvk) | ||
| } | ||
| return obj, nil | ||
| } |
There was a problem hiding this comment.
Any specific reason why this code is outside client.go?
No description provided.