Change:
<!-- Please use this template while reporting a bug and provide as much info as possible. Not doing so may result in your bug not being addressed in a timely manner. Thanks! If the matter is security related, please disclose it privately via --What happened: The endpointSliceTracker creates a set of resource versions for each service, the resource versions in the set could be deleted when endpointslices are deleted, but the set and its key in the map is never deleted, leading to memory leak. What you expected to happen: The set of resource versions should be delete from endpointSliceTracker when the service is deleted. Environment: Kubernetes version (use ): 1.18.5
There are no sig labels on this issue. Please add an appropriate label by using one of the following commands: - Please see the for a listing of the SIGs, working groups, and committees available. <detailsInstructions for interacting with me using PR comments are available . If you have questions or suggestions related to my behavior, please file an issue against the repository. </details
/cc

Discussion:
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", <ins> "//staging/src/k8s.io/apimachinery/pkg/types:go_default_library", </ins> "//staging/src/k8s.io/apimachinery/pkg/util/intstr:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/rand:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",

if apierrors.IsNotFound(err) { c.triggerTimeTracker.DeleteService(namespace, name) c.reconciler.deleteService(namespace, name) <ins> c.endpointSliceTracker.DeleteService(namespace, name) </ins> // The service has been deleted, return nil so that it won't be retried. return nil }

est.lock.Lock() defer est.lock.Unlock() <del> rrv := est.relatedResourceVersions(endpointSlice) _, ok := rrv[endpointSlice.Name] </del> <ins> rrv, ok := est.relatedResourceVersions(endpointSlice) if !ok { return false } _, ok = rrv[endpointSlice.Name] </ins> return ok }

est.lock.Lock() defer est.lock.Unlock() <del> rrv := est.relatedResourceVersions(endpointSlice) </del> <ins> rrv, ok := est.relatedResourceVersions(endpointSlice) if !ok { return true } </ins> return rrv[endpointSlice.Name] != endpointSlice.ResourceVersion }

est.lock.Lock() defer est.lock.Unlock() <del> rrv := est.relatedResourceVersions(endpointSlice) </del> <ins> rrv, ok := est.relatedResourceVersions(endpointSlice) if !ok { rrv = endpointSliceResourceVersions{} est.resourceVersionsByService[getServiceNN(endpointSlice)] = rrv } </ins> rrv[endpointSlice.Name] = endpointSlice.ResourceVersion } <ins> // DeleteService removes the set of resource versions tracked for the Service. func (est *endpointSliceTracker) DeleteService(namespace, name string) { est.lock.Lock() defer est.lock.Unlock() serviceNN := types.NamespacedName{Name: name, Namespace: namespace} delete(est.resourceVersionsByService, serviceNN) } </ins> // Delete removes the resource version in this endpointSliceTracker for the // provided EndpointSlice. func (est *endpointSliceTracker) Delete(endpointSlice *discovery.EndpointSlice) { est.lock.Lock() defer est.lock.Unlock() <del> rrv := est.relatedResourceVersions(endpointSlice) delete(rrv, endpointSlice.Name) </del> <ins> rrv, ok := est.relatedResourceVersions(endpointSlice) if ok { delete(rrv, endpointSlice.Name) } </ins> } // relatedResourceVersions returns the set of resource versions tracked for the <del> // Service corresponding to the provided EndpointSlice. If no resource versions // are currently tracked for this service, an empty set is initialized. func (est *endpointSliceTracker) relatedResourceVersions(endpointSlice *discovery.EndpointSlice) endpointSliceResourceVersions { </del> <ins> // Service corresponding to the provided EndpointSlice, and a bool to indicate // if it exists. func (est *endpointSliceTracker) relatedResourceVersions(endpointSlice *discovery.EndpointSlice) (endpointSliceResourceVersions, bool) { </ins> serviceNN := getServiceNN(endpointSlice) vers, ok := est.resourceVersionsByService[serviceNN] <del> if !ok { vers = endpointSliceResourceVersions{} est.resourceVersionsByService[serviceNN] = vers } return vers </del> <ins> return vers, ok </ins> } // getServiceNN returns a namespaced name for the Service corresponding to the

import ( "testing" <ins> "github.com/stretchr/testify/assert" </ins> discovery "k8s.io/api/discovery/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" <ins> "k8s.io/apimachinery/pkg/types" </ins> ) func TestEndpointSliceTrackerUpdate(t *testing.T) {

epSlice1DifferentRV.ResourceVersion = "rv2" testCases := map[string]struct { <del> updateParam *discovery.EndpointSlice checksParam *discovery.EndpointSlice expectHas   bool expectStale bool </del> <ins> updateParam                     *discovery.EndpointSlice checksParam                     *discovery.EndpointSlice expectHas                       bool expectStale                     bool expectResourceVersionsByService map[types.NamespacedName]endpointSliceResourceVersions </ins> }{ "same slice": { updateParam: epSlice1, checksParam: epSlice1, expectHas:   true, expectStale: false, <ins> expectResourceVersionsByService: map[types.NamespacedName]endpointSliceResourceVersions{ {Namespace: epSlice1.Namespace, Name: "svc1"}: { epSlice1.Name: epSlice1.ResourceVersion, }, }, </ins> }, "different namespace": { updateParam: epSlice1, checksParam: epSlice1DifferentNS, expectHas:   false, expectStale: true, <ins> expectResourceVersionsByService: map[types.NamespacedName]endpointSliceResourceVersions{ {Namespace: epSlice1.Namespace, Name: "svc1"}: { epSlice1.Name: epSlice1.ResourceVersion, }, }, </ins> }, "different service": { updateParam: epSlice1, checksParam: epSlice1DifferentService, expectHas:   false, expectStale: true, <ins> expectResourceVersionsByService: map[types.NamespacedName]endpointSliceResourceVersions{ {Namespace: epSlice1.Namespace, Name: "svc1"}: { epSlice1.Name: epSlice1.ResourceVersion, }, }, </ins> }, "different resource version": { updateParam: epSlice1, checksParam: epSlice1DifferentRV, expectHas:   true, expectStale: true, <ins> expectResourceVersionsByService: map[types.NamespacedName]endpointSliceResourceVersions{ {Namespace: epSlice1.Namespace, Name: "svc1"}: { epSlice1.Name: epSlice1.ResourceVersion, }, }, </ins> }, }

if esTracker.Stale(tc.checksParam) != tc.expectStale { t.Errorf("tc.tracker.Stale(%+v) == %t, expected %t", tc.checksParam, esTracker.Stale(tc.checksParam), tc.expectStale) } <ins> assert.Equal(t, tc.expectResourceVersionsByService, esTracker.resourceVersionsByService) </ins> }) } }

}) } } <ins> func TestEndpointSliceTrackerDeleteService(t *testing.T) { svcName1, svcNS1 := "svc1", "ns1" svcName2, svcNS2 := "svc2", "ns2" epSlice1 := &discovery.EndpointSlice{ ObjectMeta: metav1.ObjectMeta{ Name:            "example-1", Namespace:       svcNS1, ResourceVersion: "rv1", Labels:          map[string]string{discovery.LabelServiceName: svcName1}, }, } testCases := map[string]struct { updateParam                     *discovery.EndpointSlice deleteServiceParam              *types.NamespacedName expectHas                       bool expectStale                     bool expectResourceVersionsByService map[types.NamespacedName]endpointSliceResourceVersions }{ "same service": { updateParam:                     epSlice1, deleteServiceParam:              &types.NamespacedName{Namespace: svcNS1, Name: svcName1}, expectHas:                       false, expectStale:                     true, expectResourceVersionsByService: map[types.NamespacedName]endpointSliceResourceVersions{}, }, "different namespace": { updateParam:        epSlice1, deleteServiceParam: &types.NamespacedName{Namespace: svcNS2, Name: svcName1}, expectHas:          true, expectStale:        false, expectResourceVersionsByService: map[types.NamespacedName]endpointSliceResourceVersions{ {Namespace: epSlice1.Namespace, Name: "svc1"}: { epSlice1.Name: epSlice1.ResourceVersion, }, }, }, "different service": { updateParam:        epSlice1, deleteServiceParam: &types.NamespacedName{Namespace: svcNS1, Name: svcName2}, expectHas:          true, expectStale:        false, expectResourceVersionsByService: map[types.NamespacedName]endpointSliceResourceVersions{ {Namespace: epSlice1.Namespace, Name: "svc1"}: { epSlice1.Name: epSlice1.ResourceVersion, }, }, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { esTracker := newEndpointSliceTracker() esTracker.Update(tc.updateParam) esTracker.DeleteService(tc.deleteServiceParam.Namespace, tc.deleteServiceParam.Name) if esTracker.Has(tc.updateParam) != tc.expectHas { t.Errorf("tc.tracker.Has(%+v) == %t, expected %t", tc.updateParam, esTracker.Has(tc.updateParam), tc.expectHas) } if esTracker.Stale(tc.updateParam) != tc.expectStale { t.Errorf("tc.tracker.Stale(%+v) == %t, expected %t", tc.updateParam, esTracker.Stale(tc.updateParam), tc.expectStale) } assert.Equal(t, tc.expectResourceVersionsByService, esTracker.resourceVersionsByService) }) } } </ins>

} func expectTrackedResourceVersion(t *testing.T, tracker *endpointSliceTracker, slice *discovery.EndpointSlice, expectedRV string) { <del> rrv := tracker.relatedResourceVersions(slice) </del> <ins> rrv, _ := tracker.relatedResourceVersions(slice) </ins> rv, tracked := rrv[slice.Name] if !tracked { t.Fatalf("Expected EndpointSlice %s to be tracked", slice.Name)