unified-storage: Reduce calls to the user service (#102934)

* Create ListByIdOrUID in user service

* create UnstructuredToLegacyFolderList 

* update GetFolders to use list parser

* update GetDescendants to use list parser

* update UnstructuredToLegacyFolder to also make a single call to the user service

---------

Co-authored-by: Stephanie Hingtgen <stephanie.hingtgen@grafana.com>
This commit is contained in:
Will Assis
2025-03-27 22:01:07 +02:00
committed by GitHub
co-authored by Stephanie Hingtgen
parent 7e3efb3df2
commit 51825cfffe
13 changed files with 643 additions and 63 deletions
+20
View File
@@ -25,6 +25,7 @@ type store interface {
Insert(context.Context, *user.User) (int64, error)
GetByID(context.Context, int64) (*user.User, error)
GetByUID(ctx context.Context, uid string) (*user.User, error)
ListByIdOrUID(ctx context.Context, uids []string, ids []int64) ([]*user.User, error)
GetByLogin(context.Context, *user.GetUserByLoginQuery) (*user.User, error)
GetByEmail(context.Context, *user.GetUserByEmailQuery) (*user.User, error)
Delete(context.Context, int64) error
@@ -127,6 +128,25 @@ func (ss *sqlStore) GetByUID(ctx context.Context, uid string) (*user.User, error
return &usr, err
}
func (ss *sqlStore) ListByIdOrUID(ctx context.Context, uids []string, ids []int64) ([]*user.User, error) {
users := make([]*user.User, 0)
err := ss.db.WithDbSession(ctx, func(sess *db.Session) error {
err := sess.Table("user").In("uid", uids).OrIn("id", ids).Find(&users)
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
return users, err
}
func (ss *sqlStore) notServiceAccountFilter() string {
return fmt.Sprintf("%s.is_service_account = %s",
ss.dialect.Quote("user"),
+105 -3
View File
@@ -3,9 +3,12 @@ package userimpl
import (
"context"
"fmt"
"sort"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -848,6 +851,105 @@ func TestIntegrationUserDataAccess(t *testing.T) {
assert.Equal(t, queryResult.OrgName, "user1@test.com")
assert.Equal(t, queryResult.IsGrafanaAdmin, false)
})
t.Run("Can get users by UID list", func(t *testing.T) {
users := createFiveTestUsers(t, usrSvc, func(i int) *user.CreateUserCommand {
return &user.CreateUserCommand{
Email: fmt.Sprint("USERLISTUIDTEST", i, "@test.com"),
Name: fmt.Sprint("USERLISTUIDTEST", i),
Login: fmt.Sprint("loginUSERLISTUIDTEST", i),
IsDisabled: false,
}
})
sort.Slice(users, func(i, j int) bool {
return users[i].ID < users[j].ID
})
alluids := make([]string, 0, 5)
for _, user := range users {
alluids = append(alluids, user.UID)
}
resultOnlyUIDs, err := userStore.ListByIdOrUID(context.Background(), alluids, []int64{})
require.NoError(t, err)
sort.Slice(resultOnlyUIDs, func(i, j int) bool {
return resultOnlyUIDs[i].ID < resultOnlyUIDs[j].ID
})
require.Equal(t, len(resultOnlyUIDs), len(users))
ignoreTimeFields := cmpopts.IgnoreFields(user.User{}, "Created", "Updated", "LastSeenAt")
if diff := cmp.Diff(users, resultOnlyUIDs, ignoreTimeFields); diff != "" {
t.Errorf("structs don't match (-want +got):\n%s", diff)
}
})
t.Run("Can get users by ID list", func(t *testing.T) {
users := createFiveTestUsers(t, usrSvc, func(i int) *user.CreateUserCommand {
return &user.CreateUserCommand{
Email: fmt.Sprint("USERLISTIDTEST", i, "@test.com"),
Name: fmt.Sprint("USERLISTIDTEST", i),
Login: fmt.Sprint("loginUSERLISTIDTEST", i),
IsDisabled: false,
}
})
sort.Slice(users, func(i, j int) bool {
return users[i].ID < users[j].ID
})
allids := make([]int64, 0, 5)
for _, user := range users {
allids = append(allids, user.ID)
}
resultOnlyIDs, err := userStore.ListByIdOrUID(context.Background(), []string{}, allids)
require.NoError(t, err)
sort.Slice(resultOnlyIDs, func(i, j int) bool {
return resultOnlyIDs[i].ID < resultOnlyIDs[j].ID
})
ignoreTimeFields := cmpopts.IgnoreFields(user.User{}, "Created", "Updated", "LastSeenAt")
if diff := cmp.Diff(users, resultOnlyIDs, ignoreTimeFields); diff != "" {
t.Errorf("structs don't match (-want +got):\n%s", diff)
}
})
t.Run("Can get users by UID and ID list", func(t *testing.T) {
users := createFiveTestUsers(t, usrSvc, func(i int) *user.CreateUserCommand {
return &user.CreateUserCommand{
Email: fmt.Sprint("USERLISTUIDANDIDTEST", i, "@test.com"),
Name: fmt.Sprint("USERLISTUIDANDIDTEST", i),
Login: fmt.Sprint("loginUSERLISTUIDANDIDTEST", i),
IsDisabled: false,
}
})
sort.Slice(users, func(i, j int) bool {
return users[i].ID < users[j].ID
})
ids := make([]int64, 0, 2)
uids := make([]string, 0, 3)
for i, user := range users {
if i < 2 {
ids = append(ids, user.ID)
} else {
uids = append(uids, user.UID)
}
}
resultOnlyIDs, err := userStore.ListByIdOrUID(context.Background(), uids, ids)
require.NoError(t, err)
sort.Slice(resultOnlyIDs, func(i, j int) bool {
return resultOnlyIDs[i].ID < resultOnlyIDs[j].ID
})
ignoreTimeFields := cmpopts.IgnoreFields(user.User{}, "Created", "Updated", "LastSeenAt")
if diff := cmp.Diff(users, resultOnlyIDs, ignoreTimeFields); diff != "" {
t.Errorf("structs don't match (-want +got):\n%s", diff)
}
})
}
func TestIntegrationUserUpdate(t *testing.T) {
@@ -905,15 +1007,15 @@ func TestIntegrationUserUpdate(t *testing.T) {
})
}
func createFiveTestUsers(t *testing.T, svc user.Service, fn func(i int) *user.CreateUserCommand) []user.User {
func createFiveTestUsers(t *testing.T, svc user.Service, fn func(i int) *user.CreateUserCommand) []*user.User {
t.Helper()
users := make([]user.User, 5)
users := make([]*user.User, 5)
for i := 0; i < 5; i++ {
cmd := fn(i)
user, err := svc.Create(context.Background(), cmd)
require.Nil(t, err)
users[i] = *user
users[i] = user
}
return users
+13
View File
@@ -235,6 +235,19 @@ func (s *Service) GetByUID(ctx context.Context, query *user.GetUserByUIDQuery) (
return s.store.GetByUID(ctx, query.UID)
}
func (s *Service) ListByIdOrUID(ctx context.Context, uids []string, ids []int64) ([]*user.User, error) {
if len(uids) == 0 && len(ids) == 0 {
return []*user.User{}, nil
}
ctx, span := s.tracer.Start(ctx, "user.ListByIdOrUID", trace.WithAttributes(
attribute.StringSlice("userUIDs", uids),
attribute.Int64Slice("userIDs", ids),
))
defer span.End()
return s.store.ListByIdOrUID(ctx, uids, ids)
}
func (s *Service) GetByLogin(ctx context.Context, query *user.GetUserByLoginQuery) (*user.User, error) {
ctx, span := s.tracer.Start(ctx, "user.GetByLogin")
defer span.End()
+5
View File
@@ -274,6 +274,7 @@ type FakeUserStore struct {
ExpectedError error
ExpectedDeleteUserError error
ExpectedCountUserAccountsWithEmptyRoles int64
ExpectedListUsersByIdOrUid []*user.User
}
func newUserStoreFake() *FakeUserStore {
@@ -296,6 +297,10 @@ func (f *FakeUserStore) GetByUID(context.Context, string) (*user.User, error) {
return f.ExpectedUser, f.ExpectedError
}
func (f *FakeUserStore) ListByIdOrUID(context.Context, []string, []int64) ([]*user.User, error) {
return f.ExpectedListUsersByIdOrUid, f.ExpectedError
}
func (f *FakeUserStore) LoginConflict(context.Context, string, string) error {
return f.ExpectedError
}