GraphQL: Unlock goroutine throttle to match batch capacity (#20146)
We were throttling the amount of concurrent resolvers at a given time. The idea behind this was to avoid overloading the database with too many requests. However, with the introduction of dataloaders, this limitation actually becomes a bottleneck because all DB calls are actually batched, so we are unnecessarily throttling the amount of items that can be processed in a single batch. The only caveat with this is that now all resolvers need to backed by dataloaders, or otherwise not be queried as part of a loop. In a subsequent PR, we will be removing channel stats from under channel to be a top-level object to be returned for a given channel. ```release-note NONE ```
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
a3af488492
Коммит
fed5404166
@@ -14,6 +14,7 @@ import (
|
||||
gqlerrors "github.com/graph-gophers/graphql-go/errors"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/web"
|
||||
)
|
||||
|
||||
type graphQLInput struct {
|
||||
@@ -22,6 +23,19 @@ type graphQLInput struct {
|
||||
Variables map[string]interface{} `json:"variables"`
|
||||
}
|
||||
|
||||
// Unique type to hold our context.
|
||||
type ctxKey int
|
||||
|
||||
const (
|
||||
webCtx ctxKey = 0
|
||||
rolesLoaderCtx ctxKey = 1
|
||||
channelsLoaderCtx ctxKey = 2
|
||||
teamsLoaderCtx ctxKey = 3
|
||||
usersLoaderCtx ctxKey = 4
|
||||
)
|
||||
|
||||
const loaderBatchCapacity = web.PerPageMaximum + 100
|
||||
|
||||
//go:embed schema.graphqls
|
||||
var schemaRaw string
|
||||
|
||||
@@ -35,7 +49,8 @@ func (api *API) InitGraphQL() error {
|
||||
opts := []graphql.SchemaOpt{
|
||||
graphql.UseFieldResolvers(),
|
||||
graphql.Logger(mlog.NewGraphQLLogger(api.srv.Log)),
|
||||
graphql.MaxParallelism(5),
|
||||
graphql.MaxParallelism(loaderBatchCapacity), // This is dangerous if the query
|
||||
// uses any non-dataloader backed object. So we need to be a bit careful here.
|
||||
}
|
||||
|
||||
if isProd() {
|
||||
@@ -58,18 +73,6 @@ func (api *API) InitGraphQL() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unique type to hold our context.
|
||||
type ctxKey int
|
||||
|
||||
const (
|
||||
webCtx ctxKey = 0
|
||||
rolesLoaderCtx ctxKey = 1
|
||||
channelsLoaderCtx ctxKey = 2
|
||||
teamsLoaderCtx ctxKey = 3
|
||||
)
|
||||
|
||||
const loaderBatchCapacity = 200
|
||||
|
||||
func (api *API) graphQL(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var response *graphql.Response
|
||||
defer func() {
|
||||
@@ -111,6 +114,9 @@ func (api *API) graphQL(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
teamsLoader := dataloader.NewBatchedLoader(graphQLTeamsLoader, dataloader.WithBatchCapacity(loaderBatchCapacity))
|
||||
reqCtx = context.WithValue(reqCtx, teamsLoaderCtx, teamsLoader)
|
||||
|
||||
usersLoader := dataloader.NewBatchedLoader(graphQLUsersLoader, dataloader.WithBatchCapacity(loaderBatchCapacity))
|
||||
reqCtx = context.WithValue(reqCtx, usersLoaderCtx, usersLoader)
|
||||
|
||||
response = api.schema.Exec(reqCtx,
|
||||
params.Query,
|
||||
params.OperationName,
|
||||
|
||||
@@ -313,3 +313,12 @@ func getTeamsLoader(ctx context.Context) (*dataloader.Loader, error) {
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// getUsersLoader returns the users loader out of the context.
|
||||
func getUsersLoader(ctx context.Context) (*dataloader.Loader, error) {
|
||||
l, ok := ctx.Value(usersLoaderCtx).(*dataloader.Loader)
|
||||
if !ok {
|
||||
return nil, errors.New("no dataloader.Loader found in context")
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
@@ -32,16 +32,17 @@ func getGraphQLUser(ctx context.Context, id string) (*user, error) {
|
||||
return nil, web.NewInvalidParamError("user_id")
|
||||
}
|
||||
|
||||
canSee, appErr := c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, id)
|
||||
if appErr != nil || !canSee {
|
||||
c.SetPermissionError(model.PermissionViewMembers)
|
||||
return nil, c.Err
|
||||
loader, err := getUsersLoader(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
usr, appErr := c.App.GetUser(id)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
thunk := loader.Load(ctx, dataloader.StringKey(id))
|
||||
result, err := thunk()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
usr := result.(*model.User)
|
||||
|
||||
if c.IsSystemAdmin() || c.AppContext.Session().UserId == usr.Id {
|
||||
userTermsOfService, appErr := c.App.GetUserTermsOfService(usr.Id)
|
||||
@@ -153,3 +154,61 @@ func (u *user) Sessions(ctx context.Context) ([]*model.Session, error) {
|
||||
|
||||
return sessions, nil
|
||||
}
|
||||
|
||||
func graphQLUsersLoader(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
|
||||
stringKeys := keys.Keys()
|
||||
result := make([]*dataloader.Result, len(stringKeys))
|
||||
|
||||
c, err := getCtx(ctx)
|
||||
if err != nil {
|
||||
for i := range result {
|
||||
result[i] = &dataloader.Result{Error: err}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
users, err := getGraphQLUsers(c, stringKeys)
|
||||
if err != nil {
|
||||
for i := range result {
|
||||
result[i] = &dataloader.Result{Error: err}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
for i, user := range users {
|
||||
result[i] = &dataloader.Result{Data: user}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func getGraphQLUsers(c *web.Context, userIDs []string) ([]*model.User, error) {
|
||||
// Usually this will be called only for one user
|
||||
// and cached for the rest of the query. So it's not an issue
|
||||
// to run this in a loop.
|
||||
for _, id := range userIDs {
|
||||
canSee, appErr := c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, id)
|
||||
if appErr != nil || !canSee {
|
||||
c.SetPermissionError(model.PermissionViewMembers)
|
||||
return nil, c.Err
|
||||
}
|
||||
}
|
||||
|
||||
users, appErr := c.App.GetUsers(userIDs)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
// The users need to be in the exact same order as the input slice.
|
||||
tmp := make(map[string]*model.User)
|
||||
for _, u := range users {
|
||||
tmp[u.Id] = u
|
||||
}
|
||||
|
||||
// We reuse the same slice and just rewrite the roles.
|
||||
for i, uID := range userIDs {
|
||||
users[i] = tmp[uID]
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user