Skip to content
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

Shuffle User Order in UsersScanner #10513

Merged
merged 13 commits into from
Jan 27, 2025
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
* `cortex_ingester_tsdb_block_postings_for_matchers_cache_misses_total`
* `cortex_ingester_tsdb_block_postings_for_matchers_cache_requests_total`
* `cortex_ingester_tsdb_block_postings_for_matchers_cache_skips_total`
* [ENHANCEMENT] Compactor: Shuffle order of users in `UsersScanner` before submitting to `BlocksCleaner`. #10513
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this briefly say "why" in addition to "how"? So that unfamiliar readers understand why they might upgrade, and so on. :)

* [BUGFIX] Distributor: Use a boolean to track changes while merging the ReplicaDesc components, rather than comparing the objects directly. #10185
* [BUGFIX] Querier: fix timeout responding to query-frontend when response size is very close to `-querier.frontend-client.grpc-max-send-msg-size`. #10154
* [BUGFIX] Query-frontend and querier: show warning/info annotations in some cases where they were missing (if a lazy querier was used). #10277
Expand Down
2 changes: 1 addition & 1 deletion pkg/storage/bucket/client_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ func (m *ClientMock) MockGetAndLastModified(name, content string, lastModified t
} else {
m.On("Exists", mock.Anything, name).Return(false, err)
m.On("Get", mock.Anything, name).Return(nil, ErrObjectDoesNotExist)
m.On("Attributes", mock.Anything, name).Return(nil, ErrObjectDoesNotExist)
m.On("Attributes", mock.Anything, name).Return(objstore.ObjectAttributes{}, ErrObjectDoesNotExist)
}
}

Expand Down
5 changes: 5 additions & 0 deletions pkg/storage/tsdb/users_scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package tsdb

import (
"context"
"math/rand"
"strings"

"github.com/go-kit/log"
Expand Down Expand Up @@ -46,6 +47,10 @@ func (s *UsersScanner) ScanUsers(ctx context.Context) (users, markedForDeletion
return nil, nil, err
}

rand.Shuffle(len(users), func(i, j int) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know we've changed course on this a time or two, but ScanUsers is a general purpose accessor function used by many services, whereas this shuffle behavior is particular to the needs of compactor. I actually think we can move the shuffle to runCleanup after we call refreshOwnedUsers, with a brief handrail comment noting why we'd do such a thing.

users[i], users[j] = users[j], users[i]
})

// Check users for being owned by instance, and split users into non-deleted and deleted.
// We do these checks after listing all users, to improve cacheability of Iter (result is only cached at the end of Iter call).
for ix := 0; ix < len(users); {
Expand Down
33 changes: 29 additions & 4 deletions pkg/storage/tsdb/users_scanner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package tsdb
import (
"context"
"errors"
"fmt"
"path"
"testing"

Expand Down Expand Up @@ -36,10 +37,10 @@ func TestUsersScanner_ScanUsers_ShouldReturnedOwnedUsersOnly(t *testing.T) {
}

func TestUsersScanner_ScanUsers_ShouldReturnUsersForWhichOwnerCheckOrTenantDeletionCheckFailed(t *testing.T) {
expected := []string{"user-1", "user-2"}
users := []string{"user-1", "user-2"}

bucketClient := &bucket.ClientMock{}
bucketClient.MockIter("", expected, nil)
bucketClient.MockIter("", users, nil)
bucketClient.MockExists(path.Join("user-1", TenantDeletionMarkPath), false, nil)
bucketClient.MockExists(path.Join("user-2", TenantDeletionMarkPath), false, errors.New("fail"))

Expand All @@ -50,11 +51,12 @@ func TestUsersScanner_ScanUsers_ShouldReturnUsersForWhichOwnerCheckOrTenantDelet
s := NewUsersScanner(bucketClient, isOwned, log.NewNopLogger())
actual, deleted, err := s.ScanUsers(context.Background())
require.NoError(t, err)
assert.Equal(t, expected, actual)
dmwilson-grafana marked this conversation as resolved.
Show resolved Hide resolved
assert.ElementsMatch(t, actual, users)
assert.Empty(t, deleted)
}

func TestUsersScanner_ScanUsers_ShouldNotReturnPrefixedUsedByMimirInternals(t *testing.T) {
users := []string{"user-1", "user-2"}
bucketClient := &bucket.ClientMock{}
bucketClient.MockIter("", []string{"user-1", "user-2", bucket.MimirInternalsPrefix}, nil)
bucketClient.MockExists(path.Join("user-1", TenantDeletionMarkPath), false, nil)
Expand All @@ -63,5 +65,28 @@ func TestUsersScanner_ScanUsers_ShouldNotReturnPrefixedUsedByMimirInternals(t *t
s := NewUsersScanner(bucketClient, AllUsers, log.NewNopLogger())
actual, _, err := s.ScanUsers(context.Background())
require.NoError(t, err)
assert.Equal(t, []string{"user-1", "user-2"}, actual)
assert.ElementsMatch(t, actual, users)
}

func TestUsersScanner_ScanUsers_ShouldReturnRandomizedOrder(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what this test is testing. It doesn't seem like it's ensuring a random order.

Copy link
Contributor Author

@dmwilson-grafana dmwilson-grafana Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without the shuffle, the order of the input (users) would be the same as the order of actual. On L94, I check that we have the same set of users (as you mentioned, this can be changed to assert.ElementsMatch) and on L97 I check that it's actually been shuffled , i.e. users != actual.

A better test might involve setting a specific rand.Source and checking that it shuffles to a specific order.

var users = make([]string, 20)
for i := 0; i < len(users); i++ {
users[i] = fmt.Sprintf("user-%d", i)
}
bucketClient := &bucket.ClientMock{}
bucketClient.MockIter("", users, nil)
for i := 0; i < len(users); i++ {
bucketClient.MockExists(path.Join(users[i], TenantDeletionMarkPath), false, nil)
}

isOwned := func(_ string) (bool, error) {
return true, nil
}

s := NewUsersScanner(bucketClient, isOwned, log.NewNopLogger())
actual, deleted, err := s.ScanUsers(context.Background())
require.NoError(t, err)
assert.ElementsMatch(t, actual, users)
assert.Empty(t, deleted)
assert.NotEqual(t, actual, users)
}
Loading