Change:
The in apiserver grows infinitely without ever purging serviceKeys which are not referenced by any APIServices. /sig api-machinery /cc /assign a limited size cache APIService your services Spec.Service field to a new address step 2 and watch the list grow No response_ <details26-1.27 </details<detailsN/A </details<details</details<detailsN/A </details<detailsN/A </details<detailsN/A </details
/triage accepted
/assign
what do we want to the limit the cache size by?
the cache currently doesn't purge s which are not being used by an
is this still an issue? should it be resolved before enabling by default?
Can you elaborate? I am not sure what you mean by enabling by default?
Yes That's the cache for the downloaded discovery for each apiservice. The list probably doesn't grow very large. Fix is in . Considering the pain of non-aggregated discovery on some users, and the time they have had to wait to get the feature, I'd say the trade-off is that this isn't nearly as bad.
It's also worth noting that the cache should only grow for Aggregated APIServices that themselves expose aggregated discovery endpoint. We don't store a cache entry here otherwise. The list doesn't grow unless the aggregated discovery document was successfully fetched. This is a very rare case and especially at release a minuscule number of people on the bleeding edge might run into it. And even then it is not likely to be changing the so often once it is working enough to store a cache entry.

Discussion:
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" <ins> "k8s.io/apimachinery/pkg/util/sets" </ins> "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/endpoints"

}, stopCh) } <ins> // Takes a snapshot of all currently used services by known APIServices and // purges the cache entries of those not present in the snapshot. func (dm *discoveryManager) removeUnusedServices() { usedServiceKeys := sets.Set[serviceKey]{} func() { dm.servicesLock.Lock() defer dm.servicesLock.Unlock() // Mark all non-local APIServices as dirty for _, info := range dm.apiServices { usedServiceKeys.Insert(info.service) } }() // Avoids double lock. It is okay if a service is added/removed between these // functions. This is just a cache and that should be infrequent. func() { dm.resultsLock.Lock() defer dm.resultsLock.Unlock() for key := range dm.cachedResults { if !usedServiceKeys.Has(key) { delete(dm.cachedResults, key) } } }() } </ins> // Adds an APIService to be tracked by the discovery manager. If the APIService // is already known func (dm *discoveryManager) AddAPIService(apiService *apiregistrationv1.APIService, handler http.Handler) {

lastMarkedDirty: time.Now(), service:         newServiceKey(*apiService.Spec.Service), }) <ins> dm.removeUnusedServices() </ins> dm.dirtyAPIServiceQueue.Add(apiService.Name) } func (dm *discoveryManager) RemoveAPIService(apiServiceName string) { if dm.setInfoForAPIService(apiServiceName, nil) != nil { // mark dirty if there was actually something deleted <ins> dm.removeUnusedServices() </ins> dm.dirtyAPIServiceQueue.Add(apiServiceName) } }

checkAPIGroups(t, apiGroup, parsed) } <ins> func TestServiceGC(t *testing.T) { service := discoveryendpoint.NewResourceManager("apis") aggregatedResourceManager := discoveryendpoint.NewResourceManager("apis") aggregatedManager := newDiscoveryManager(aggregatedResourceManager) testCtx, cancel := context.WithCancel(context.Background()) defer cancel() go aggregatedManager.Run(testCtx.Done(), nil) aggregatedManager.AddAPIService(&apiregistrationv1.APIService{ ObjectMeta: metav1.ObjectMeta{ Name: "v1.stable.example.com", }, Spec: apiregistrationv1.APIServiceSpec{ Group:   "stable.example.com", Version: "v1", Service: &apiregistrationv1.ServiceReference{ Name: "test-service", }, }, }, service) require.True(t, waitForQueueComplete(testCtx.Done(), aggregatedManager)) // Lookup size of cache getCacheLen := func() int { aggregatedManager.resultsLock.Lock() defer aggregatedManager.resultsLock.Unlock() return len(aggregatedManager.cachedResults) } require.Equal(t, 1, getCacheLen()) // Change the service of the same APIService a bit to create duplicate entry aggregatedManager.AddAPIService(&apiregistrationv1.APIService{ ObjectMeta: metav1.ObjectMeta{ Name: "v1.stable.example.com", }, Spec: apiregistrationv1.APIServiceSpec{ Group:   "stable.example.com", Version: "v1", Service: &apiregistrationv1.ServiceReference{ Name: "test-service-changed", }, }, }, service) require.True(t, waitForQueueComplete(testCtx.Done(), aggregatedManager)) require.Equal(t, 1, getCacheLen()) } </ins> // Test that a handler associated with an APIService gets pinged after the // APIService has been marked as dirty func TestDirty(t *testing.T) {