Repository navigation
perf(group): count members in sql and preload group inbounds when syncing users - #966
Closed
FakharzadehH wants to merge 1 commit into
Closed
FakharzadehH wants to merge 1 commit into
FakharzadehH wants to merge 1 commit into
Conversation
… group updates Root causes: - Group.total_users was len(Group.users), so listing groups (get_group), fetching one (get_group_by_id) and every create/modify/bulk-disable (load_group_attrs) loaded every member User row just to produce a count. total_users is now a SQL COUNT (query_expression + with_expression, or one COUNT in load_group_attrs); Group.users is never loaded for these paths. - Modifying a group re-syncs all of its users to the nodes, but get_users only selectin-loaded User.groups, not Group.inbounds. User.inbounds() then fell back to one "SELECT DISTINCT inbounds.tag ..." per user whenever a user belonged to a group whose inbounds were not loaded. get_users gains load_group_inbounds to nested-eager-load them (a handful of batched queries), and the group operation sync paths use it and skip the unused usage_logs. 20,000-user group on SQLite: adding an inbound 3.17s / 10,132 queries -> 0.53s / 92 queries; group list 0.20s / 4 queries -> 0.06s / 3 queries, without loading any User rows.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adding an inbound to a group with many users (
PUT /api/group/{id}) is very slow, and listing groups loads far more data than it needs. Two root causes:Group.total_userswaslen(self.users)(app/db/models.py).get_groupusedselectinload(Group.users)(app/db/crud/group.py), andload_group_attrsawaitedgroup.awaitable_attrs.usersforget_group_by_id(used by get/modify/delete),create_group,modify_groupand the bulk enable/disable path. Every one of those loaded allUserrows for a number.total_usersis now a SQLCOUNT:with_expression(Group._total_users_query, Group.total_users)inget_group/get_group_by_id, and a singleCOUNTinload_group_attrs. Thetotal_usershybrid uses the loaded value and falls back tolen(users), so API responses are unchanged. Samequery_expressionmechanism asUser._reseted_usage_query.GroupOperation.modify_group(app/operation/group.py) re-syncs every member.get_usersonly didselectinload(User.groups)(app/db/crud/user.py), soGroup.inboundswas never loaded andUser.inbounds()fell back to oneSELECT DISTINCT inbounds.tag ...per user. The sync path also loadedusage_logsfor every user, which it does not use.get_users(load_group_inbounds=False)nestsselectinload(Group.inbounds)underUser.groups. The group operation paths that feedsync_users(modify, remove, bulk remove by id, bulk enable/disable) now passload_group_inbounds=True, load_usage_logs=False.Type of change
Checklist
query_expressionadds no column;alembic checkreports no new upgrade operations.)Testing
ruff check .reports 4 errors (app/routers/passkey.pyx2,tests/api/test_user.py,tests/test_review_users_unit.py). They are identical on a cleanorigin/devcheckout and are not touched by this PR.Only SQLite was run locally; the PostgreSQL and MariaDB jobs were not run.
New tests (
tests/test_group_user_loading.py, crud level, in-memory SQLite) assert thatget_group/get_group_by_id/load_group_attrsreturn correcttotal_userswithout loadingGroup.usersor touching theuserstable, and thatget_users(load_group_inbounds=True)leaves nothing forUser.inbounds()to query.Before / after
Measured on in-memory SQLite with one big group plus a second group sharing half its members, node dispatch stubbed, calling
GroupOperation.modify_groupandget_all_groups. SQLite hides network round trips and wideusersrows, so real deployments should gain more.modify_group)modify_group)Userrows loaded)total_usersvalues and the modify response (inbound_tags, count, 20,000 users dispatched) were identical before and after.Screenshots
Not applicable.
Notes for reviewers
total_userswith a SQL count without loadingGroup.users" item.total_usershybrid on the model with aquery_expressionand adds the reverse association indexes. Both touch the same lines inmodels.py/crud/group.py, so one will need a rebase. This PR keeps thelen(users)fallback in the hybrid and adds the count inload_group_attrs, which perf(groups): add reverse group membership indexes #772'swith_expressionapproach is compatible with. Without perf(groups): add reverse group membership indexes #772's(groups_id, user_id)index theCOUNTstill scansusers_groups_association.load_group_attrsnow takesdbfirst andload_total_usersreplacesload_users;get_group_by_idno longer takesload_users. All callers are updated (crud/group.py,operation/group.py).sync_users_allocationsbuilds oneIN (...)over all affected user ids. On PostgreSQL/asyncpg this could exceed the 32,767-parameter limit at roughly 33k+ users. Not tested.remove_groupandbulk_remove_groups_by_idfirst callget_usersonly to collect usernames, loading full users.create_user_template/modify_user_templatestill load group members (perf(templates): avoid loading group members and batch list relations #944).