-
Notifications
You must be signed in to change notification settings - Fork 1.3k
fix: drop N+1 db query on template ACL available #25465
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
Changes from all commits
ab8e0cb
afc652e
b225641
97078b9
fb8ac71
4b502ec
b06490c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -375,9 +375,19 @@ func (c *Client) UpdateTemplateACL(ctx context.Context, templateID uuid.UUID, re | |
| return nil | ||
| } | ||
|
|
||
| // TemplateACLAvailable returns available users + groups that can be assigned template perms | ||
| func (c *Client) TemplateACLAvailable(ctx context.Context, templateID uuid.UUID) (ACLAvailable, error) { | ||
| res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templates/%s/acl/available", templateID), nil) | ||
| // TemplateACLAvailable returns available users + groups that can be assigned | ||
| // template perms. The optional req controls the q/limit/offset query | ||
| // parameters applied server-side; pass codersdk.UsersRequest{} when no | ||
| // filtering is desired. | ||
| func (c *Client) TemplateACLAvailable(ctx context.Context, templateID uuid.UUID, req UsersRequest) (ACLAvailable, error) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it intended to accept
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's so weird, but the query does get parsed by the serverside endpoint for users. Part of the user query applies to groups, but the entire query applies to users in the same list 🤷 |
||
| res, err := c.Request( | ||
| ctx, | ||
| http.MethodGet, | ||
| fmt.Sprintf("/api/v2/templates/%s/acl/available", templateID), | ||
| nil, | ||
| req.Pagination.asRequestOption(), | ||
| req.asRequestOption(), | ||
| ) | ||
| if err != nil { | ||
| return ACLAvailable{}, err | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ import ( | |
| "golang.org/x/xerrors" | ||
|
|
||
| "cdr.dev/slog/v3" | ||
| agpl "github.com/coder/coder/v2/coderd" | ||
| "github.com/coder/coder/v2/coderd/audit" | ||
| "github.com/coder/coder/v2/coderd/database" | ||
| "github.com/coder/coder/v2/coderd/database/db2sdk" | ||
|
|
@@ -17,6 +18,7 @@ import ( | |
| "github.com/coder/coder/v2/coderd/httpmw" | ||
| "github.com/coder/coder/v2/coderd/rbac/acl" | ||
| "github.com/coder/coder/v2/coderd/rbac/policy" | ||
| "github.com/coder/coder/v2/coderd/searchquery" | ||
| "github.com/coder/coder/v2/coderd/util/slice" | ||
| "github.com/coder/coder/v2/codersdk" | ||
| ) | ||
|
|
@@ -50,39 +52,63 @@ func (api *API) templateAvailablePermissions(rw http.ResponseWriter, r *http.Req | |
| return | ||
| } | ||
|
|
||
| // Apply the same q/limit semantics to groups as the users half of this response. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to update the swagger docs for
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think so. This behavior feels more intuitive. Before the groups ignored the search query entirely, which made no sense |
||
| // The query semantics are defined for the users, which is awkward. But we can | ||
| // just reuse the search part of the query which is a fuzzy match. | ||
| userFilter, verr := searchquery.Users(r.URL.Query().Get("q")) | ||
| if len(verr) > 0 { | ||
| httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ | ||
| Message: "Invalid user search query.", | ||
| Validations: verr, | ||
| }) | ||
| return | ||
| } | ||
| groupPagination, ok := agpl.ParsePagination(rw, r) | ||
| if !ok { | ||
|
Emyrk marked this conversation as resolved.
|
||
| return | ||
| } | ||
|
|
||
| // Perm check is the template update check. | ||
| // nolint:gocritic | ||
| groups, err := api.Database.GetGroups(dbauthz.AsSystemRestricted(ctx), database.GetGroupsParams{ | ||
| OrganizationID: template.OrganizationID, | ||
| Search: userFilter.Search, | ||
| // #nosec G115 - Pagination limits are small and fit in int32 | ||
| LimitOpt: int32(groupPagination.Limit), | ||
| }) | ||
| if err != nil { | ||
| httpapi.InternalServerError(rw, err) | ||
| return | ||
| } | ||
|
|
||
| sdkGroups := make([]codersdk.Group, 0, len(groups)) | ||
| for _, group := range groups { | ||
| // nolint:gocritic | ||
| members, err := api.Database.GetGroupMembersByGroupID(dbauthz.AsSystemRestricted(ctx), database.GetGroupMembersByGroupIDParams{ | ||
| GroupID: group.Group.ID, | ||
| IncludeSystem: false, | ||
| }) | ||
| if err != nil { | ||
| httpapi.InternalServerError(rw, err) | ||
| return | ||
| } | ||
| // Fetch member counts for all groups in a single query to avoid an | ||
| // N+1 lookup pattern that was making this endpoint extremely slow on | ||
| // deployments with many groups. The per-group member lists are | ||
| // intentionally not populated here: callers of this endpoint only | ||
| // surface total_member_count (see Group.TotalMemberCount, which is | ||
| // already documented as the canonical value). | ||
| groupIDs := make([]uuid.UUID, len(groups)) | ||
| for i, g := range groups { | ||
| groupIDs[i] = g.Group.ID | ||
| } | ||
|
|
||
| // nolint:gocritic | ||
| memberCount, err := api.Database.GetGroupMembersCountByGroupID(dbauthz.AsSystemRestricted(ctx), database.GetGroupMembersCountByGroupIDParams{ | ||
| GroupID: group.Group.ID, | ||
| IncludeSystem: false, | ||
| }) | ||
| if err != nil { | ||
| httpapi.InternalServerError(rw, err) | ||
| return | ||
| } | ||
| // nolint:gocritic // Same justification as the GetGroups call above. | ||
| countRows, err := api.Database.GetGroupMembersCountByGroupIDs(dbauthz.AsSystemRestricted(ctx), database.GetGroupMembersCountByGroupIDsParams{ | ||
| GroupIds: groupIDs, | ||
| IncludeSystem: false, | ||
| }) | ||
| if err != nil { | ||
| httpapi.InternalServerError(rw, err) | ||
| return | ||
| } | ||
| countByGroup := make(map[uuid.UUID]int64, len(countRows)) | ||
| for _, row := range countRows { | ||
| countByGroup[row.GroupID] = row.MemberCount | ||
| } | ||
|
|
||
| sdkGroups = append(sdkGroups, db2sdk.Group(group, members, int(memberCount))) | ||
| sdkGroups := make([]codersdk.Group, 0, len(groups)) | ||
| for _, group := range groups { | ||
| sdkGroups = append(sdkGroups, db2sdk.Group(group, nil, int(countByGroup[group.Group.ID]))) | ||
| } | ||
|
|
||
| httpapi.Write(ctx, rw, http.StatusOK, codersdk.ACLAvailable{ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have a good method around doing this broad rbac check.
This is to avoid and n+1 query to fetch the groups