Change:
This was the root cause of rancher/rancher. In , when is called, it is given the oldVersion of an object and the newVersion of the object. It then uses the indexers to calculate the old values (from the old version) and the new updated values. In cases where are size of 1, are of size greater than 1, and contain the sole value from , the key will be deleted for the old value will be deleted and won't be re-. This is due to the two for loops at the end of the method which are used for optimization. When the first for loop is processed, . Because of this, the key is deleted from the index for this value. In the second for loop, the , and for at least one of the values . The continue statement causes the key to not be to the index for this value (and since the value was deleted in the prior for loop, it no longer exists in the index for this value), despite the fact that the indicated that this is the desired state. Since specified that this value was still valid for this object, the key should not be deleted from the index for this value, or it should be re- when it is re-discovered in . Either way, the end state of the index should include this key in the set for the specified value. I've created a which demonstrates the issue (branch is ). To use it (assuming that you have a running kubernetes cluster to use with): the controller: . the foo crd: the controller (assumes that you have $KUBECONFIG set to the file path of your kubeconfig file). an example foo with only one label: . Observe, in the logs of the running sample-controller, that the indexer was able to locate this foo using the indexer. the foo to include a second label: . Observe, in the logs of the running sample-controller, that the indexer was not able to locate the foo using the first key any longer, despite the fact that the label was still on the object. The sample-controller fork adds an indexer for foo objects which uses the indexerFunction of . This function looks for labels that have a key with a prefix of and uses their values as index keys for the foo objects. For example, if I have a with a label of , the indexer will allow you to retrieve using . The controller's method has been altered to just ensure that the foos can be retrieved using the indexer for all valid labels. If the controller can't retrieve foo by using the value from a valid label, then it returns an error (it tries for all valid labels). This was originally discovered in a fork of client-go that we maintain. After we reverted the changes introduced by , our issues stopped occurring. N/A - This was introduced in and appears to be in all versions of the client v0.23+. N/A - issue is with client-go No response No response No response No response
/sig api-machinery
I think that you have background in this issue/area of the code. Would you mind taking a look at this issue and the PR that I have up to fix it? Please let me know if I've misunderstood anything.
/assign Would you mind to take a look? Thank you /triage accepted
Thanks for reporting the issue. will address this issue
Reopening - since we need to cherrypick back to 1.23
Cherrypick is merged - closing.

Discussion:
c.indices[name] = index } <ins> if len(indexValues) == 1 && len(oldIndexValues) == 1 && indexValues[0] == oldIndexValues[0] { // We optimize for the most common case where indexFunc returns a single value which has not been changed continue } </ins> for _, value := range oldIndexValues { <del> // We optimize for the most common case where indexFunc returns a single value. if len(indexValues) == 1 && value == indexValues[0] { continue } </del> c.deleteKeyFromIndex(key, value, index) } for _, value := range indexValues { <del> // We optimize for the most common case where indexFunc returns a single value. if len(oldIndexValues) == 1 && value == oldIndexValues[0] { continue } </del> c.addKeyToIndex(key, value, index) } }

import ( "fmt" <ins> "strings" </ins> "testing" <ins> "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" </ins> ) func TestThreadSafeStoreDeleteRemovesEmptySetsFromIndex(t *testing.T) {

} } <ins> func TestThreadSafeStoreIndexingFunctionsWithMultipleValues(t *testing.T) { testIndexer := "testIndexer" indexers := Indexers{ testIndexer: func(obj interface{}) ([]string, error) { return strings.Split(obj.(string), ","), nil }, } indices := Indices{} store := NewThreadSafeStore(indexers, indices).(*threadSafeMap) store.Add("key1", "foo") store.Add("key2", "bar") assert := assert.New(t) compare := func(key string, expected []string) error { values := store.indices[testIndexer][key].List() if cmp.Equal(values, expected) { return nil } return fmt.Errorf("unexpected index for key %s, diff=%s", key, cmp.Diff(values, expected)) } assert.NoError(compare("foo", []string{"key1"})) assert.NoError(compare("bar", []string{"key2"})) store.Update("key2", "foo,bar") assert.NoError(compare("foo", []string{"key1", "key2"})) assert.NoError(compare("bar", []string{"key2"})) store.Update("key1", "foo,bar") assert.NoError(compare("foo", []string{"key1", "key2"})) assert.NoError(compare("bar", []string{"key1", "key2"})) store.Add("key3", "foo,bar,baz") assert.NoError(compare("foo", []string{"key1", "key2", "key3"})) assert.NoError(compare("bar", []string{"key1", "key2", "key3"})) assert.NoError(compare("baz", []string{"key3"})) store.Update("key1", "foo") assert.NoError(compare("foo", []string{"key1", "key2", "key3"})) assert.NoError(compare("bar", []string{"key2", "key3"})) assert.NoError(compare("baz", []string{"key3"})) store.Update("key2", "bar") assert.NoError(compare("foo", []string{"key1", "key3"})) assert.NoError(compare("bar", []string{"key2", "key3"})) assert.NoError(compare("baz", []string{"key3"})) store.Delete("key1") assert.NoError(compare("foo", []string{"key3"})) assert.NoError(compare("bar", []string{"key2", "key3"})) assert.NoError(compare("baz", []string{"key3"})) store.Delete("key3") assert.NoError(compare("foo", []string{})) assert.NoError(compare("bar", []string{"key2"})) assert.NoError(compare("baz", []string{})) } </ins> func BenchmarkIndexer(b *testing.B) { testIndexer := "testIndexer"