Skip to content

Commit 225b61e

Browse files
committed
feat: performance improvement
1 parent 682c27f commit 225b61e

File tree

7 files changed

+30
-129
lines changed

7 files changed

+30
-129
lines changed

internal/core/sync.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,6 @@ func (ss *SyncService) SyncGroupsAndTheirMembers(ctx context.Context) error {
153153
}
154154
}
155155

156-
// after be sure all the SCIM side is aligned with the identity provider side
157-
// we can update the state with the last data coming from the reconciliation
158156
newState := model.StateBuilder().
159157
WithCodeVersion(version.Version).
160158
WithLastSync(time.Now().Format(time.RFC3339)).

internal/core/sync_test.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,6 @@ func TestSyncService_SyncGroupsAndTheirMembers(t *testing.T) {
134134
assert.Equal(t, 0, len(state.Resources.GroupsMembers.Resources))
135135
assert.NotEqual(t, "", state.LastSync)
136136
assert.NotEqual(t, "", state.HashCode)
137-
assert.Equal(t, "", state.CodeVersion)
138137
assert.Equal(t, model.StateSchemaVersion, state.SchemaVersion)
139138
})
140139

@@ -539,7 +538,6 @@ func TestSyncService_SyncGroupsAndTheirMembers(t *testing.T) {
539538
assert.Equal(t, 4, len(state.Resources.GroupsMembers.Resources))
540539
assert.NotEqual(t, "", state.LastSync)
541540
assert.NotEqual(t, "", state.HashCode)
542-
assert.Equal(t, "", state.CodeVersion)
543541
assert.Equal(t, model.StateSchemaVersion, state.SchemaVersion)
544542
assert.Equal(t, 2, state.Resources.Groups.Items)
545543
assert.Equal(t, 2, state.Resources.Users.Items)

internal/idp/idp.go

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ type GoogleProviderService interface {
3434
GetUser(ctx context.Context, userID string) (*admin.User, error)
3535

3636
// Batch operations for performance optimization
37-
GetUsersBatch(ctx context.Context, emails []string) ([]*admin.User, error)
3837
ListGroupMembersBatch(ctx context.Context, groupIDs []string, queries ...google.GetGroupMembersOption) (map[string][]*admin.Member, error)
3938
}
4039

@@ -182,32 +181,27 @@ func (i *IdentityProvider) GetUsersByGroupsMembers(ctx context.Context, gmr *mod
182181
return uResult, nil
183182
}
184183

185-
// Collect unique emails first
186-
uniqEmails := make(map[string]struct{}, len(gmr.Resources))
187-
emails := make([]string, 0, len(gmr.Resources))
184+
uniqUsers := make(map[string]struct{}, len(gmr.Resources))
185+
pUsers := make([]*model.User, 0, len(gmr.Resources))
188186
for _, groupMembers := range gmr.Resources {
189187
for _, member := range groupMembers.Resources {
190-
if _, ok := uniqEmails[member.Email]; !ok {
191-
uniqEmails[member.Email] = struct{}{}
192-
emails = append(emails, member.Email)
188+
if _, ok := uniqUsers[member.Email]; !ok {
189+
uniqUsers[member.Email] = struct{}{}
190+
191+
u, err := i.ps.GetUser(ctx, member.Email)
192+
if err != nil {
193+
return nil, fmt.Errorf("idp: error getting user: %+v, email: %s, error: %w", member.IPID, member.Email, err)
194+
}
195+
gu := buildUser(u)
196+
197+
pUsers = append(pUsers, gu)
193198
}
194199
}
195200
}
196201

197-
// Use batch operation instead of individual GetUser calls
198-
pUsers, err := i.ps.GetUsersBatch(ctx, emails)
199-
if err != nil {
200-
return nil, fmt.Errorf("idp: error getting users batch: %w", err)
201-
}
202-
203-
// Convert to model users
204-
syncUsers := make([]*model.User, len(pUsers))
205-
for i, usr := range pUsers {
206-
gu := buildUser(usr)
207-
syncUsers[i] = gu
208-
}
202+
pUsersResult := model.UsersResultBuilder().WithResources(pUsers).Build()
209203

210-
pUsersResult := model.UsersResultBuilder().WithResources(syncUsers).Build()
204+
slog.Debug("idp: GetUsersByGroupsMembers()", "users", len(pUsers))
211205

212206
return pUsersResult, nil
213207
}

internal/idp/idp_test.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,8 @@ func TestGetUsersByGroupsMembers(t *testing.T) {
559559
name: "Should return error",
560560
prepare: func(f *fields) {
561561
ctx := context.Background()
562-
f.ds.EXPECT().GetUsersBatch(ctx, gomock.Eq([]string{"user.1@mail.com"})).Return(nil, errors.New("test error")).Times(1)
562+
// Expect a single user fetch and return error
563+
f.ds.EXPECT().GetUser(ctx, "user.1@mail.com").Return(nil, errors.New("test error")).Times(1)
563564
},
564565
args: args{
565566
ctx: context.Background(),
@@ -582,6 +583,7 @@ func TestGetUsersByGroupsMembers(t *testing.T) {
582583
name: "Should return UsersResult and no error",
583584
prepare: func(f *fields) {
584585
ctx := context.Background()
586+
// Expect individual user fetches for each unique member email
585587
googleUser1 := &admin.User{
586588
Id: "1",
587589
PrimaryEmail: "user.1@mail.com",
@@ -635,7 +637,10 @@ func TestGetUsersByGroupsMembers(t *testing.T) {
635637
// },
636638
}
637639

638-
f.ds.EXPECT().GetUsersBatch(ctx, gomock.Eq([]string{"user.1@mail.com", "user.2@mail.com", "user.3@mail.com", "user.4@mail.com"})).Return([]*admin.User{googleUser1, googleUser2, googleUser3, googleUser4}, nil).Times(1)
640+
f.ds.EXPECT().GetUser(ctx, "user.1@mail.com").Return(googleUser1, nil).Times(1)
641+
f.ds.EXPECT().GetUser(ctx, "user.2@mail.com").Return(googleUser2, nil).Times(1)
642+
f.ds.EXPECT().GetUser(ctx, "user.3@mail.com").Return(googleUser3, nil).Times(1)
643+
f.ds.EXPECT().GetUser(ctx, "user.4@mail.com").Return(googleUser4, nil).Times(1)
639644
},
640645
args: args{
641646
ctx: context.Background(),

internal/scim/scim.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"log/slog"
77
"math/rand"
8+
"sync"
89
"time"
910

1011
"github.com/slashdevops/idp-scim-sync/internal/model"
@@ -516,6 +517,8 @@ func (s *Provider) GetGroupsMembersBruteForce(ctx context.Context, gr *model.Gro
516517
// set a limit to the number of concurrent goroutines
517518
// to avoid hitting the API rate limit
518519
g.SetLimit(5)
520+
// protect concurrent access to membersByGroup
521+
var mu sync.Mutex
519522

520523
// brute force implemented here thanks to the fxxckin' aws sso scim api
521524
// iterate over each group and user and check if the user is a member of the group
@@ -546,15 +549,18 @@ func (s *Provider) GetGroupsMembersBruteForce(ctx context.Context, gr *model.Gro
546549

547550
// AWS SSO SCIM API, it doesn't return the member into the Resources array
548551
if lgr.TotalResults > 0 {
552+
// build member and append to group map
549553
m := model.MemberBuilder().
550554
WithIPID(user.IPID).
551555
WithSCIMID(user.SCIMID).
552556
WithEmail(user.Emails[0].Value).
553557
Build()
554-
555558
if user.Active {
556559
m.Status = "ACTIVE"
557560
}
561+
mu.Lock()
562+
membersByGroup[group.SCIMID] = append(membersByGroup[group.SCIMID], m)
563+
mu.Unlock()
558564
}
559565

560566
return nil

mocks/idp/idp_mocks.go

Lines changed: 0 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/google/google.go

Lines changed: 2 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"fmt"
66
"log/slog"
77
"net/http"
8-
"strings"
98
"sync"
109

1110
"golang.org/x/oauth2"
@@ -143,7 +142,9 @@ func (ds *DirectoryService) ListUsers(ctx context.Context, query []string) ([]*a
143142
if len(query) > 0 {
144143
for _, q := range query {
145144
if q != "" {
145+
slog.Debug("google: Listing users with query", "query", q)
146146
err := ds.svc.Users.List().Query(q).Customer("my_customer").Fields(listUsersRequiredFields).Pages(ctx, func(users *admin.Users) error {
147+
slog.Debug("google: Retrieved users page", "page_size", len(users.Users))
147148
u = append(u, users.Users...)
148149
return nil
149150
})
@@ -304,92 +305,6 @@ func (ds *DirectoryService) GetGroup(ctx context.Context, groupID string) (*admi
304305
return g, nil
305306
}
306307

307-
// GetUsersBatch retrieves multiple users by their email addresses using batch queries.
308-
// This method is optimized for performance by using multiple ListUsers calls with email queries
309-
// instead of making individual GetUser calls.
310-
func (ds *DirectoryService) GetUsersBatch(ctx context.Context, emails []string) ([]*admin.User, error) {
311-
if len(emails) == 0 {
312-
return []*admin.User{}, nil
313-
}
314-
315-
// Use chunked approach for better reliability and performance
316-
// Optimal chunk size balances API complexity vs number of requests
317-
const chunkSize = 15
318-
319-
var allUsers []*admin.User
320-
321-
slog.Debug("google: GetUsersBatch starting chunked processing", "total_emails", len(emails), "chunk_size", chunkSize) // Process emails in chunks to avoid overly complex OR queries
322-
for i := 0; i < len(emails); i += chunkSize {
323-
end := i + chunkSize
324-
if end > len(emails) {
325-
end = len(emails)
326-
}
327-
328-
chunk := emails[i:end]
329-
330-
// Create OR query for this chunk
331-
emailQueries := make([]string, len(chunk))
332-
for j, email := range chunk {
333-
emailQueries[j] = fmt.Sprintf("email:%s", email)
334-
}
335-
336-
query := strings.Join(emailQueries, " OR ")
337-
338-
slog.Debug("google: processing chunk", "chunk_index", i/chunkSize+1, "chunk_size", len(chunk), "chunk_start", i)
339-
340-
// Execute ListUsers for this chunk
341-
users, err := ds.ListUsers(ctx, []string{query})
342-
if err != nil {
343-
// If chunk query fails, fall back to individual calls for this chunk only
344-
slog.Warn("google: GetUsersBatch chunk failed, falling back to individual calls for chunk",
345-
"error", err, "chunk_size", len(chunk), "chunk_start", i)
346-
347-
individualUsers, individualErr := ds.getUsersIndividually(ctx, chunk)
348-
if individualErr != nil {
349-
return nil, fmt.Errorf("google: both batch and individual calls failed: batch_error=%w, individual_error=%v", err, individualErr)
350-
}
351-
users = individualUsers
352-
} else if len(users) == 0 {
353-
// If chunk query succeeds but returns no users, also fall back to individual calls
354-
slog.Warn("google: GetUsersBatch chunk returned no users, falling back to individual calls for chunk",
355-
"chunk_size", len(chunk), "chunk_start", i)
356-
357-
individualUsers, individualErr := ds.getUsersIndividually(ctx, chunk)
358-
if individualErr != nil {
359-
slog.Warn("google: individual calls also failed for chunk", "error", individualErr)
360-
// Continue with empty users rather than failing completely
361-
} else {
362-
users = individualUsers
363-
}
364-
}
365-
366-
// Accumulate results from this chunk
367-
allUsers = append(allUsers, users...)
368-
slog.Debug("google: chunk completed", "chunk_users", len(users), "total_users_so_far", len(allUsers))
369-
}
370-
371-
slog.Debug("google: GetUsersBatch completed", "total_users_returned", len(allUsers), "total_emails_requested", len(emails))
372-
373-
return allUsers, nil
374-
}
375-
376-
// getUsersIndividually retrieves users one by one using GetUser calls
377-
func (ds *DirectoryService) getUsersIndividually(ctx context.Context, emails []string) ([]*admin.User, error) {
378-
users := make([]*admin.User, 0, len(emails))
379-
380-
for _, email := range emails {
381-
user, err := ds.GetUser(ctx, email)
382-
if err != nil {
383-
// Log the error but continue with other users
384-
slog.Warn("google: failed to get individual user", "email", email, "error", err)
385-
continue
386-
}
387-
users = append(users, user)
388-
}
389-
390-
return users, nil
391-
}
392-
393308
// ListGroupMembersBatch retrieves members for multiple groups concurrently.
394309
// Returns a map where keys are group IDs and values are slices of members.
395310
func (ds *DirectoryService) ListGroupMembersBatch(ctx context.Context, groupIDs []string, queries ...GetGroupMembersOption) (map[string][]*admin.Member, error) {

0 commit comments

Comments
 (0)