Skip to content

[PM-21031] Optimize GET Members endpoint performance #5907

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

Merged
merged 33 commits into from
Jul 23, 2025

Conversation

r-tome
Copy link
Contributor

@r-tome r-tome commented Jun 2, 2025

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-21031

📔 Objective

This PR improves the performance of the GET /organizations/{orgId}/users endpoint behind the MembersGetEndpointOptimization feature flag. When enabled, it significantly reduces DB round trips and improves query times.

This is the benchmark result from running OrganizationUsersControllerPerformanceTest, comparing the current (unoptimized) version with the new (optimized) version using the feature flag:

  • Seed: 60000 seats
  • Unoptimized: 90474 ms
  • Optimized: 19745 ms
  • Improvement: 78.2%

Summary of Changes

  • Optimized Endpoint Methods

    • Get_vNext() and GetAccountRecoveryEnrolledUsers_vNext()
      • Both use Task.WhenAll() to run multiple DB queries in parallel
      • Speeds up retrieval of 2FA and claimed domain status
  • Repository Enhancements

    • GetManyDetailsByOrganizationAsync_vNext()
      • Uses OrganizationUserUserDetails_ReadByOrganizationId_V2
      • Returns users, groups, and collections in a single DB call
    • GetManyByOrganizationWithClaimedDomainsAsync_vNext()
      • Uses OrganizationUser_ReadByOrganizationIdWithClaimedDomains_V2
      • Leverages CTE for more efficient domain matching
  • Performance Gains

    • Parallel query execution reduces latency
    • Consolidated DB calls improve scalability
    • Noticeable speed-up for orgs with large user counts
  • Integration Tests

    • Added tests in OrganizationUserRepositoryTests.cs
    • Ensure vNext methods preserve existing behavior while improving performance

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

Copy link

codecov bot commented Jun 2, 2025

Codecov Report

Attention: Patch coverage is 64.16185% with 62 lines in your changes missing coverage. Please review.

Project coverage is 52.05%. Comparing base (947ae8d) to head (bcf4eb2).
Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...anizationUsers/OrganizationUserUserDetailsQuery.cs 1.61% 60 Missing and 1 partial ⚠️
...ionUsers/GetOrganizationUsersClaimedStatusQuery.cs 83.33% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5907      +/-   ##
==========================================
+ Coverage   48.52%   52.05%   +3.53%     
==========================================
  Files        1734     1734              
  Lines       76764    76932     +168     
  Branches     6852     6864      +12     
==========================================
+ Hits        37246    40044    +2798     
+ Misses      38021    35320    -2701     
- Partials     1497     1568      +71     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@r-tome r-tome marked this pull request as ready for review June 2, 2025 14:48
@r-tome r-tome requested review from a team as code owners June 2, 2025 14:48
@r-tome r-tome requested a review from jrmccannon June 2, 2025 14:48
Copy link
Contributor

github-actions bot commented Jun 2, 2025

Logo
Checkmarx One – Scan Summary & Details14bb1c9f-6dc2-40e5-92dc-cd0f52d4fef6

New Issues (1)

Checkmarx found the following issues in this Pull Request

Severity Issue Source File / Package Checkmarx Insight
HIGH Path_Traversal /src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs: 56
detailsMethod PostLicenseAsync at line 56 of /src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs gets dynamic data from the model...
ID: gsOHJYoGf23vh%2BwNdCZQWci4Gmw%3D
Attack Vector
Fixed Issues (1)

Great job! The following issues were fixed in this Pull Request

Severity Issue Source File / Package
MEDIUM CSRF /src/Api/AdminConsole/Controllers/GroupsController.cs: 167

Copy link
Contributor

@jrmccannon jrmccannon left a comment

Choose a reason for hiding this comment

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

Looks good. Just had a question around the domain splitting in the CTE

WITH CTE_UserWithDomain AS (
SELECT
OU.*,
SUBSTRING(U.Email, CHARINDEX('@', U.Email) + 1, LEN(U.Email)) AS EmailDomain
Copy link
Contributor

Choose a reason for hiding this comment

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

An additional idea might to be to make this a computed column so we could index against it. Check with @rkac-bw to see if that's necessary at all. We've been making improvements to sprocs for getting the domain out of the email, so maybe it's time to do it. 🤷

Copy link
Contributor

@rkac-bw rkac-bw Jun 3, 2025

Choose a reason for hiding this comment

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

So creating a computed column on 12 million rows might have an extended down time during migration, can we not just create a column and have a migration that populates it in batches to reduce locking and then parse in the code to insert domain name?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That sounds like a great idea to me. I'll work on the batch script

Copy link
Contributor

@rkac-bw rkac-bw Jun 4, 2025

Choose a reason for hiding this comment

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

A second thought on this, @withinfocus suggested an indexed view and I tested it out and it is non blocking on creation:

CREATE VIEW dbo.UserEmailDomain
WITH SCHEMABINDING
AS
SELECT 
    Id,
    Email,
    SUBSTRING(Email, CHARINDEX('@', Email) + 1, LEN(Email)) AS EmailDomain
FROM dbo.[User]
WHERE Email IS NOT NULL 
    AND CHARINDEX('@', Email) > 0


CREATE UNIQUE CLUSTERED INDEX IX_UserEmailDomain_Id 
ON dbo.UserEmailDomain (Id);

Copy link
Contributor

Choose a reason for hiding this comment

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

We call these "details views" generally and you'll see some around. What's new for us here is being schema-bound, so we should think through how these are applied because the view will need to be dropped before column changes happen.

Copy link
Member

Choose a reason for hiding this comment

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

@rkac-bw and @withinfocus - any further thoughts on our direction here?

Copy link
Contributor

Choose a reason for hiding this comment

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

After being out and re-reading the thread, the schema-bound view stands out to me as the cleanest with respect to data presence (and lack of duplication) as well as maintenance. Yes, there's a little more to do when working with one but we'll see impacts and bugs immediately with what we have in CI. Is there something I am missing besides what Thomas stated around its recreation?

I like where the third solution could be headed but that's a significant refactor the team would need to take on -- is that something you want to do?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The consensus seems to be that the schema-bound view is the simplest and best approach for now.
@eliykat, would you be okay with me going ahead with this for now? We can revisit the idea of the third option as part of a future refactor if it makes sense then.

Copy link
Contributor

@rkac-bw rkac-bw Jul 3, 2025

Choose a reason for hiding this comment

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

I did some performance testing and found that rewriting query as CTE, using with recompile (due to small number of large orgs skew) and three non clustered indexes that it out performs the indexed view

Jira Task: PR-5907 Performance Analysis: OrganizationUser Domain Filtering

This will avoid schemabinding and deployment issues

Copy link
Contributor

Choose a reason for hiding this comment

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

Unless @withinfocus or any one else has comments I think we should go ahead with rewriting query as CTE, using with recompile (due to small number of large orgs skew) and three non clustered indexes and the view without schema binding, let me now if you have questions

Comment on lines +33 to +35
var organizationUsersWithClaimedDomain = _featureService.IsEnabled(FeatureFlagKeys.MembersGetEndpointOptimization)
? await _organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync_vNext(organizationId)
: await _organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organizationId);
Copy link
Contributor

Choose a reason for hiding this comment

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

🌱 Run this in parallel and wait for them before L38?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought about it but the organizationAbility value comes from the cache, so it's quick to access. If if (organizationAbility is { Enabled: true, UseOrganizationDomains: true }) returns false there's no need to query the database.

Copy link
Contributor

Choose a reason for hiding this comment

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

Cache may have issues for that lookup https://bitwarden.atlassian.net/wiki/x/iYDibg

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thats an issue on the cache mechanism, right? If so, that seems to be out of scope here.

Copy link
Contributor

Choose a reason for hiding this comment

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

r-tome added 12 commits June 9, 2025 10:45
…stom user types in OrganizationUserUserDetailsQuery.
…rformance

# Conflicts:
#	src/Core/Constants.cs
…that uses UserEmailDomainView to fetch Email domains
…tored procedure to use UserEmailDomainView for email domain extraction, improving query efficiency and clarity.
…etManyDetailsByOrganizationAsync method, clarifying its purpose and performance optimizations. Added remarks for better understanding of its functionality.
…Domain_V2.sql to adhere to coding standards.
@r-tome r-tome requested a review from withinfocus June 18, 2025 14:22
@r-tome r-tome requested a review from withinfocus June 24, 2025 08:19
Copy link
Contributor

@withinfocus withinfocus left a comment

Choose a reason for hiding this comment

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

@rkac-bw please give this a final review.

@r-tome r-tome requested review from withinfocus and jrmccannon July 7, 2025 15:00
@r-tome
Copy link
Contributor Author

r-tome commented Jul 7, 2025

I have implemented the suggestions by @rkac-bw above in #5907 (comment)

@r-tome r-tome added needs-qa and removed needs-qa labels Jul 16, 2025
…rformance

# Conflicts:
#	src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs
jrmccannon
jrmccannon previously approved these changes Jul 21, 2025
END
GO

IF NOT EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_OrganizationDomain_Org_VerifiedDomain')
Copy link
Contributor

Choose a reason for hiding this comment

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

Check uses wrong name (IX_OrganizationDomain_Org_VerifiedDomain) but creates
IX_OrganizationDomain_OrganizationId_VerifiedDate

  • This will create duplicate indexes on every migration run

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nicely spotted! Its fixed

Copy link
Contributor

@rkac-bw rkac-bw left a comment

Choose a reason for hiding this comment

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

LGTM

@r-tome r-tome requested a review from jrmccannon July 22, 2025 14:01
@withinfocus withinfocus removed their request for review July 22, 2025 21:33
Copy link
Contributor

@withinfocus withinfocus left a comment

Choose a reason for hiding this comment

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

Just clearing my approval here.

@r-tome r-tome merged commit acd556d into main Jul 23, 2025
34 of 35 checks passed
@r-tome r-tome deleted the ac/pm-21031/optimize-get-members-endpoint-performance branch July 23, 2025 09:04
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants