MM-43143: Dataloaders for roles (#19936)
We use a dataloader to optimize roles loading. https://mattermost.atlassian.net/browse/MM-43143 ```release-note NONE ``` Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
ba2f116ed1
Коммит
56dfcddff5
@@ -9,6 +9,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/graph-gophers/dataloader/v6"
|
||||
graphql "github.com/graph-gophers/graphql-go"
|
||||
gqlerrors "github.com/graph-gophers/graphql-go/errors"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
@@ -58,7 +59,12 @@ func (api *API) InitGraphQL() error {
|
||||
}
|
||||
|
||||
// Unique type to hold our context.
|
||||
type ctxKey struct{}
|
||||
type ctxKey int
|
||||
|
||||
const (
|
||||
webCtx ctxKey = 0
|
||||
rolesLoaderCtx ctxKey = 1
|
||||
)
|
||||
|
||||
func (api *API) graphQL(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var response *graphql.Response
|
||||
@@ -90,7 +96,10 @@ func (api *API) graphQL(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Populate the context with required info.
|
||||
reqCtx := r.Context()
|
||||
reqCtx = context.WithValue(reqCtx, ctxKey{}, c)
|
||||
reqCtx = context.WithValue(reqCtx, webCtx, c)
|
||||
|
||||
rolesLoader := dataloader.NewBatchedLoader(graphQLRolesLoader, dataloader.WithBatchCapacity(200))
|
||||
reqCtx = context.WithValue(reqCtx, rolesLoaderCtx, rolesLoader)
|
||||
|
||||
response = api.schema.Exec(reqCtx,
|
||||
params.Query,
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/graph-gophers/dataloader/v6"
|
||||
"github.com/mattermost/mattermost-server/v6/app"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/web"
|
||||
@@ -278,9 +279,18 @@ func (*resolver) ChannelMembers(ctx context.Context, args struct {
|
||||
// Kind of an anti-pattern, but there are lots of methods attached to *web.Context
|
||||
// so we use it for now.
|
||||
func getCtx(ctx context.Context) (*web.Context, error) {
|
||||
c, ok := ctx.Value(ctxKey{}).(*web.Context)
|
||||
c, ok := ctx.Value(webCtx).(*web.Context)
|
||||
if !ok {
|
||||
return nil, errors.New("no web.Context found in context")
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// getRolesLoader returns the roles loader out of the context.
|
||||
func getRolesLoader(ctx context.Context) (*dataloader.Loader, error) {
|
||||
l, ok := ctx.Value(rolesLoaderCtx).(*dataloader.Loader)
|
||||
if !ok {
|
||||
return nil, errors.New("no dataloader.Loader found in context")
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/graph-gophers/dataloader/v6"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/web"
|
||||
)
|
||||
@@ -65,12 +66,24 @@ func (cm *channelMember) Channel(ctx context.Context) (*channel, error) {
|
||||
}
|
||||
|
||||
func (cm *channelMember) Roles_(ctx context.Context) ([]*model.Role, error) {
|
||||
c, err := getCtx(ctx)
|
||||
loader, err := getRolesLoader(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return getGraphQLRoles(c, strings.Fields(cm.Roles))
|
||||
thunk := loader.LoadMany(ctx, dataloader.NewKeysFromStrings(strings.Fields(cm.Roles)))
|
||||
results, errs := thunk()
|
||||
// All errors are the same. We just return the first one.
|
||||
if len(errs) > 0 && errs[0] != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roles := make([]*model.Role, len(results))
|
||||
for i, res := range results {
|
||||
roles[i] = res.(*model.Role)
|
||||
}
|
||||
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
func (cm *channelMember) Cursor() *string {
|
||||
@@ -79,6 +92,28 @@ func (cm *channelMember) Cursor() *string {
|
||||
return model.NewString(encoded)
|
||||
}
|
||||
|
||||
func graphQLRolesLoader(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
|
||||
stringKeys := keys.Keys()
|
||||
result := make([]*dataloader.Result, len(stringKeys))
|
||||
|
||||
c, err := getCtx(ctx)
|
||||
if err != nil {
|
||||
result[0] = &dataloader.Result{Error: err}
|
||||
return result
|
||||
}
|
||||
|
||||
roles, err := getGraphQLRoles(c, stringKeys)
|
||||
if err != nil {
|
||||
result[0] = &dataloader.Result{Error: err}
|
||||
return result
|
||||
}
|
||||
|
||||
for i, role := range roles {
|
||||
result[i] = &dataloader.Result{Data: role}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func getGraphQLRoles(c *web.Context, roleNames []string) ([]*model.Role, error) {
|
||||
cleanedRoleNames, valid := model.CleanRoleNames(roleNames)
|
||||
if !valid {
|
||||
@@ -91,6 +126,17 @@ func getGraphQLRoles(c *web.Context, roleNames []string) ([]*model.Role, error)
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
// The roles need to be in the exact same order as the input slice.
|
||||
tmp := make(map[string]*model.Role)
|
||||
for _, r := range roles {
|
||||
tmp[r.Name] = r
|
||||
}
|
||||
|
||||
// We reuse the same slice and just rewrite the roles.
|
||||
for i, roleName := range roleNames {
|
||||
roles[i] = tmp[roleName]
|
||||
}
|
||||
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/graph-gophers/dataloader/v6"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
|
||||
@@ -60,10 +61,22 @@ func (tm *teamMember) SidebarCategories(ctx context.Context) ([]*model.SidebarCa
|
||||
|
||||
// match with api4.getRolesByNames
|
||||
func (tm *teamMember) Roles_(ctx context.Context) ([]*model.Role, error) {
|
||||
c, err := getCtx(ctx)
|
||||
loader, err := getRolesLoader(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return getGraphQLRoles(c, strings.Fields(tm.Roles))
|
||||
thunk := loader.LoadMany(ctx, dataloader.NewKeysFromStrings(strings.Fields(tm.Roles)))
|
||||
results, errs := thunk()
|
||||
// All errors are the same. We just return the first one.
|
||||
if len(errs) > 0 && errs[0] != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roles := make([]*model.Role, len(results))
|
||||
for i, res := range results {
|
||||
roles[i] = res.(*model.Role)
|
||||
}
|
||||
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
@@ -143,3 +143,85 @@ func TestGraphQLChannelsLeft(t *testing.T) {
|
||||
assert.Len(t, q.ChannelsLeft, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGraphQLRolesLoader(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_GRAPHQL", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_GRAPHQL")
|
||||
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
var q struct {
|
||||
User struct {
|
||||
ID string `json:"id"`
|
||||
Roles []struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"Name"`
|
||||
} `json:"roles"`
|
||||
} `json:"user"`
|
||||
ChannelMembers []struct {
|
||||
MsgCount float64 `json:"msgCount"`
|
||||
Roles []struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"Name"`
|
||||
} `json:"roles"`
|
||||
} `json:"channelMembers"`
|
||||
TeamMembers []struct {
|
||||
SchemeUser bool `json:"schemeUser"`
|
||||
Roles []struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"Name"`
|
||||
} `json:"roles"`
|
||||
}
|
||||
}
|
||||
|
||||
input := graphQLInput{
|
||||
OperationName: "channelMembers",
|
||||
Query: `
|
||||
query channelMembers {
|
||||
user(id: "me") {
|
||||
id
|
||||
username
|
||||
roles {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
channelMembers(userId: "me") {
|
||||
msgCount
|
||||
roles {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
teamMembers(userId: "me") {
|
||||
schemeUser
|
||||
roles {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
}
|
||||
|
||||
resp, err := th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
|
||||
require.Len(t, q.User.Roles, 1)
|
||||
assert.Equal(t, "system_user", q.User.Roles[0].Name)
|
||||
|
||||
require.Len(t, q.ChannelMembers, 5)
|
||||
for _, cm := range q.ChannelMembers {
|
||||
require.Len(t, cm.Roles, 1)
|
||||
assert.Equal(t, "channel_user", cm.Roles[0].Name)
|
||||
}
|
||||
|
||||
require.Len(t, q.TeamMembers, 1)
|
||||
for _, tm := range q.TeamMembers {
|
||||
require.Len(t, tm.Roles, 1)
|
||||
assert.Equal(t, "team_user", tm.Roles[0].Name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/graph-gophers/dataloader/v6"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/web"
|
||||
)
|
||||
@@ -66,17 +67,29 @@ func getGraphQLUser(ctx context.Context, id string) (*user, error) {
|
||||
|
||||
// match with api4.getRolesByNames
|
||||
func (u *user) Roles(ctx context.Context) ([]*model.Role, error) {
|
||||
c, err := getCtx(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roleNames := u.GetRoles()
|
||||
if len(roleNames) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return getGraphQLRoles(c, roleNames)
|
||||
loader, err := getRolesLoader(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
thunk := loader.LoadMany(ctx, dataloader.NewKeysFromStrings(roleNames))
|
||||
results, errs := thunk()
|
||||
// All errors are the same. We just return the first one.
|
||||
if len(errs) > 0 && errs[0] != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roles := make([]*model.Role, len(results))
|
||||
for i, res := range results {
|
||||
roles[i] = res.(*model.Role)
|
||||
}
|
||||
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
// match with api4.getPreferences
|
||||
|
||||
2
go.mod
2
go.mod
@@ -41,6 +41,7 @@ require (
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/schema v1.2.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/graph-gophers/dataloader/v6 v6.0.0
|
||||
github.com/graph-gophers/graphql-go v1.3.0
|
||||
github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c
|
||||
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
|
||||
@@ -51,7 +52,6 @@ require (
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-plugin v1.4.3
|
||||
github.com/hashicorp/go-sockaddr v1.0.2 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/hashicorp/memberlist v0.3.1
|
||||
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 // indirect
|
||||
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba
|
||||
|
||||
3
go.sum
3
go.sum
@@ -715,6 +715,8 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/graph-gophers/dataloader/v6 v6.0.0 h1:qBpmq3B8PIQesoh0EJXKGfw+ulMUb+KFl4IZOe9ScWg=
|
||||
github.com/graph-gophers/dataloader/v6 v6.0.0/go.mod h1:J15OZSnOoZgMkijpbZcwCmglIDYqlUiTEE1xLPbyqZM=
|
||||
github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0=
|
||||
github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
@@ -1180,6 +1182,7 @@ github.com/otiai10/mint v1.3.2/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw=
|
||||
github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
|
||||
1
vendor/github.com/graph-gophers/dataloader/v6/.gitignore
сгенерированный
поставляемый
Обычный файл
1
vendor/github.com/graph-gophers/dataloader/v6/.gitignore
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1 @@
|
||||
vendor/
|
||||
14
vendor/github.com/graph-gophers/dataloader/v6/.travis.yml
сгенерированный
поставляемый
Обычный файл
14
vendor/github.com/graph-gophers/dataloader/v6/.travis.yml
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,14 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.15
|
||||
- 1.14
|
||||
|
||||
install:
|
||||
- go mod install
|
||||
|
||||
script:
|
||||
- go test -v -race -coverprofile=coverage.txt -covermode=atomic
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
21
vendor/github.com/graph-gophers/dataloader/v6/LICENSE
сгенерированный
поставляемый
Обычный файл
21
vendor/github.com/graph-gophers/dataloader/v6/LICENSE
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Nick Randall
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
99
vendor/github.com/graph-gophers/dataloader/v6/MIGRATE.md
сгенерированный
поставляемый
Обычный файл
99
vendor/github.com/graph-gophers/dataloader/v6/MIGRATE.md
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,99 @@
|
||||
## Upgrade from v1 to v2
|
||||
The only difference between v1 and v2 is that we added use of [context](https://golang.org/pkg/context).
|
||||
|
||||
```diff
|
||||
- loader.Load(key string) Thunk
|
||||
+ loader.Load(ctx context.Context, key string) Thunk
|
||||
- loader.LoadMany(keys []string) ThunkMany
|
||||
+ loader.LoadMany(ctx context.Context, keys []string) ThunkMany
|
||||
```
|
||||
|
||||
```diff
|
||||
- type BatchFunc func([]string) []*Result
|
||||
+ type BatchFunc func(context.Context, []string) []*Result
|
||||
```
|
||||
|
||||
## Upgrade from v2 to v3
|
||||
```diff
|
||||
// dataloader.Interface as added context.Context to methods
|
||||
- loader.Prime(key string, value interface{}) Interface
|
||||
+ loader.Prime(ctx context.Context, key string, value interface{}) Interface
|
||||
- loader.Clear(key string) Interface
|
||||
+ loader.Clear(ctx context.Context, key string) Interface
|
||||
```
|
||||
|
||||
```diff
|
||||
// cache interface as added context.Context to methods
|
||||
type Cache interface {
|
||||
- Get(string) (Thunk, bool)
|
||||
+ Get(context.Context, string) (Thunk, bool)
|
||||
- Set(string, Thunk)
|
||||
+ Set(context.Context, string, Thunk)
|
||||
- Delete(string) bool
|
||||
+ Delete(context.Context, string) bool
|
||||
Clear()
|
||||
}
|
||||
```
|
||||
|
||||
## Upgrade from v3 to v4
|
||||
```diff
|
||||
// dataloader.Interface as now allows interace{} as key rather than string
|
||||
- loader.Load(context.Context, key string) Thunk
|
||||
+ loader.Load(ctx context.Context, key interface{}) Thunk
|
||||
- loader.LoadMany(context.Context, key []string) ThunkMany
|
||||
+ loader.LoadMany(ctx context.Context, keys []interface{}) ThunkMany
|
||||
- loader.Prime(context.Context, key string, value interface{}) Interface
|
||||
+ loader.Prime(ctx context.Context, key interface{}, value interface{}) Interface
|
||||
- loader.Clear(context.Context, key string) Interface
|
||||
+ loader.Clear(ctx context.Context, key interface{}) Interface
|
||||
```
|
||||
|
||||
```diff
|
||||
// cache interface now allows interface{} as key instead of string
|
||||
type Cache interface {
|
||||
- Get(context.Context, string) (Thunk, bool)
|
||||
+ Get(context.Context, interface{}) (Thunk, bool)
|
||||
- Set(context.Context, string, Thunk)
|
||||
+ Set(context.Context, interface{}, Thunk)
|
||||
- Delete(context.Context, string) bool
|
||||
+ Delete(context.Context, interface{}) bool
|
||||
Clear()
|
||||
}
|
||||
```
|
||||
|
||||
## Upgrade from v4 to v5
|
||||
```diff
|
||||
// dataloader.Interface as now allows interace{} as key rather than string
|
||||
- loader.Load(context.Context, key interface{}) Thunk
|
||||
+ loader.Load(ctx context.Context, key Key) Thunk
|
||||
- loader.LoadMany(context.Context, key []interface{}) ThunkMany
|
||||
+ loader.LoadMany(ctx context.Context, keys Keys) ThunkMany
|
||||
- loader.Prime(context.Context, key interface{}, value interface{}) Interface
|
||||
+ loader.Prime(ctx context.Context, key Key, value interface{}) Interface
|
||||
- loader.Clear(context.Context, key interface{}) Interface
|
||||
+ loader.Clear(ctx context.Context, key Key) Interface
|
||||
```
|
||||
|
||||
```diff
|
||||
// cache interface now allows interface{} as key instead of string
|
||||
type Cache interface {
|
||||
- Get(context.Context, interface{}) (Thunk, bool)
|
||||
+ Get(context.Context, Key) (Thunk, bool)
|
||||
- Set(context.Context, interface{}, Thunk)
|
||||
+ Set(context.Context, Key, Thunk)
|
||||
- Delete(context.Context, interface{}) bool
|
||||
+ Delete(context.Context, Key) bool
|
||||
Clear()
|
||||
}
|
||||
```
|
||||
|
||||
## Upgrade from v5 to v6
|
||||
|
||||
We add major version release because we switched to using Go Modules from dep,
|
||||
and drop build tags for older versions of Go (1.9).
|
||||
|
||||
The preferred import method includes the major version tag.
|
||||
|
||||
```go
|
||||
import "github.com/graph-gophers/dataloader/v6"
|
||||
```
|
||||
48
vendor/github.com/graph-gophers/dataloader/v6/README.md
сгенерированный
поставляемый
Обычный файл
48
vendor/github.com/graph-gophers/dataloader/v6/README.md
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,48 @@
|
||||
# DataLoader
|
||||
[](https://godoc.org/github.com/graph-gophers/dataloader)
|
||||
[](https://travis-ci.org/graph-gophers/dataloader)
|
||||
|
||||
This is an implementation of [Facebook's DataLoader](https://github.com/facebook/dataloader) in Golang.
|
||||
|
||||
## Install
|
||||
`go get -u github.com/graph-gophers/dataloader`
|
||||
|
||||
## Usage
|
||||
```go
|
||||
// setup batch function
|
||||
batchFn := func(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
|
||||
var results []*dataloader.Result
|
||||
// do some async work to get data for specified keys
|
||||
// append to this list resolved values
|
||||
return results
|
||||
}
|
||||
|
||||
// create Loader with an in-memory cache
|
||||
loader := dataloader.NewBatchedLoader(batchFn)
|
||||
|
||||
/**
|
||||
* Use loader
|
||||
*
|
||||
* A thunk is a function returned from a function that is a
|
||||
* closure over a value (in this case an interface value and error).
|
||||
* When called, it will block until the value is resolved.
|
||||
*/
|
||||
thunk := loader.Load(context.TODO(), dataloader.StringKey("key1")) // StringKey is a convenience method that make wraps string to implement `Key` interface
|
||||
result, err := thunk()
|
||||
if err != nil {
|
||||
// handle data error
|
||||
}
|
||||
|
||||
log.Printf("value: %#v", result)
|
||||
```
|
||||
|
||||
### Don't need/want to use context?
|
||||
You're welcome to install the v1 version of this library.
|
||||
|
||||
## Cache
|
||||
This implementation contains a very basic cache that is intended only to be used for short lived DataLoaders (i.e. DataLoaders that ony exsist for the life of an http request). You may use your own implementation if you want.
|
||||
|
||||
> it also has a `NoCache` type that implements the cache interface but all methods are noop. If you do not wish to cache anything.
|
||||
|
||||
## Examples
|
||||
There are a few basic examples in the example folder.
|
||||
80
vendor/github.com/graph-gophers/dataloader/v6/TRACE.md
сгенерированный
поставляемый
Обычный файл
80
vendor/github.com/graph-gophers/dataloader/v6/TRACE.md
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,80 @@
|
||||
# Adding a new trace backend.
|
||||
|
||||
If you whant to add a new tracing backend all you need to do is implement the
|
||||
`Tracer` interface and pass it as an option to the dataloader on initialization.
|
||||
|
||||
As an example, this is how you could implement it to an OpenCensus backend.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
exp "go.opencensus.io/examples/exporter"
|
||||
"github.com/nicksrandall/dataloader"
|
||||
"go.opencensus.io/trace"
|
||||
)
|
||||
|
||||
// OpenCensusTracer Tracer implements a tracer that can be used with the Open Tracing standard.
|
||||
type OpenCensusTracer struct{}
|
||||
|
||||
// TraceLoad will trace a call to dataloader.LoadMany with Open Tracing
|
||||
func (OpenCensusTracer) TraceLoad(ctx context.Context, key dataloader.Key) (context.Context, dataloader.TraceLoadFinishFunc) {
|
||||
cCtx, cSpan := trace.StartSpan(ctx, "Dataloader: load")
|
||||
cSpan.AddAttributes(
|
||||
trace.StringAttribute("dataloader.key", key.String()),
|
||||
)
|
||||
return cCtx, func(thunk dataloader.Thunk) {
|
||||
// TODO: is there anything we should do with the results?
|
||||
cSpan.End()
|
||||
}
|
||||
}
|
||||
|
||||
// TraceLoadMany will trace a call to dataloader.LoadMany with Open Tracing
|
||||
func (OpenCensusTracer) TraceLoadMany(ctx context.Context, keys dataloader.Keys) (context.Context, dataloader.TraceLoadManyFinishFunc) {
|
||||
cCtx, cSpan := trace.StartSpan(ctx, "Dataloader: loadmany")
|
||||
cSpan.AddAttributes(
|
||||
trace.StringAttribute("dataloader.keys", strings.Join(keys.Keys(), ",")),
|
||||
)
|
||||
return cCtx, func(thunk dataloader.ThunkMany) {
|
||||
// TODO: is there anything we should do with the results?
|
||||
cSpan.End()
|
||||
}
|
||||
}
|
||||
|
||||
// TraceBatch will trace a call to dataloader.LoadMany with Open Tracing
|
||||
func (OpenCensusTracer) TraceBatch(ctx context.Context, keys dataloader.Keys) (context.Context, dataloader.TraceBatchFinishFunc) {
|
||||
cCtx, cSpan := trace.StartSpan(ctx, "Dataloader: batch")
|
||||
cSpan.AddAttributes(
|
||||
trace.StringAttribute("dataloader.keys", strings.Join(keys.Keys(), ",")),
|
||||
)
|
||||
return cCtx, func(results []*dataloader.Result) {
|
||||
// TODO: is there anything we should do with the results?
|
||||
cSpan.End()
|
||||
}
|
||||
}
|
||||
|
||||
func batchFunc(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
|
||||
// ...loader logic goes here
|
||||
}
|
||||
|
||||
func main(){
|
||||
//initialize an example exporter that just logs to the console
|
||||
trace.ApplyConfig(trace.Config{
|
||||
DefaultSampler: trace.AlwaysSample(),
|
||||
})
|
||||
trace.RegisterExporter(&exp.PrintExporter{})
|
||||
// initialize the dataloader with your new tracer backend
|
||||
loader := dataloader.NewBatchedLoader(batchFunc, dataloader.WithTracer(OpenCensusTracer{}))
|
||||
// initialize a context since it's not receiving one from anywhere else.
|
||||
ctx, span := trace.StartSpan(context.TODO(), "Span Name")
|
||||
defer span.End()
|
||||
// request from the dataloader as usual
|
||||
value, err := loader.Load(ctx, dataloader.StringKey(SomeID))()
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Don't forget to initialize the exporters of your choice and register it with `trace.RegisterExporter(&exporterInstance)`.
|
||||
28
vendor/github.com/graph-gophers/dataloader/v6/cache.go
сгенерированный
поставляемый
Обычный файл
28
vendor/github.com/graph-gophers/dataloader/v6/cache.go
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,28 @@
|
||||
package dataloader
|
||||
|
||||
import "context"
|
||||
|
||||
// The Cache interface. If a custom cache is provided, it must implement this interface.
|
||||
type Cache interface {
|
||||
Get(context.Context, Key) (Thunk, bool)
|
||||
Set(context.Context, Key, Thunk)
|
||||
Delete(context.Context, Key) bool
|
||||
Clear()
|
||||
}
|
||||
|
||||
// NoCache implements Cache interface where all methods are noops.
|
||||
// This is useful for when you don't want to cache items but still
|
||||
// want to use a data loader
|
||||
type NoCache struct{}
|
||||
|
||||
// Get is a NOOP
|
||||
func (c *NoCache) Get(context.Context, Key) (Thunk, bool) { return nil, false }
|
||||
|
||||
// Set is a NOOP
|
||||
func (c *NoCache) Set(context.Context, Key, Thunk) { return }
|
||||
|
||||
// Delete is a NOOP
|
||||
func (c *NoCache) Delete(context.Context, Key) bool { return false }
|
||||
|
||||
// Clear is a NOOP
|
||||
func (c *NoCache) Clear() { return }
|
||||
26
vendor/github.com/graph-gophers/dataloader/v6/codecov.yml
сгенерированный
поставляемый
Обычный файл
26
vendor/github.com/graph-gophers/dataloader/v6/codecov.yml
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,26 @@
|
||||
codecov:
|
||||
notify:
|
||||
require_ci_to_pass: true
|
||||
comment:
|
||||
behavior: default
|
||||
layout: header, diff
|
||||
require_changes: false
|
||||
coverage:
|
||||
precision: 2
|
||||
range:
|
||||
- 70.0
|
||||
- 100.0
|
||||
round: down
|
||||
status:
|
||||
changes: false
|
||||
patch: true
|
||||
project: true
|
||||
parsers:
|
||||
gcov:
|
||||
branch_detection:
|
||||
conditional: true
|
||||
loop: true
|
||||
macro: false
|
||||
method: false
|
||||
javascript:
|
||||
enable_partials: false
|
||||
492
vendor/github.com/graph-gophers/dataloader/v6/dataloader.go
сгенерированный
поставляемый
Обычный файл
492
vendor/github.com/graph-gophers/dataloader/v6/dataloader.go
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,492 @@
|
||||
// Package dataloader is an implimentation of facebook's dataloader in go.
|
||||
// See https://github.com/facebook/dataloader for more information
|
||||
package dataloader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Interface is a `DataLoader` Interface which defines a public API for loading data from a particular
|
||||
// data back-end with unique keys such as the `id` column of a SQL table or
|
||||
// document name in a MongoDB database, given a batch loading function.
|
||||
//
|
||||
// Each `DataLoader` instance should contain a unique memoized cache. Use caution when
|
||||
// used in long-lived applications or those which serve many users with
|
||||
// different access permissions and consider creating a new instance per
|
||||
// web request.
|
||||
type Interface interface {
|
||||
Load(context.Context, Key) Thunk
|
||||
LoadMany(context.Context, Keys) ThunkMany
|
||||
Clear(context.Context, Key) Interface
|
||||
ClearAll() Interface
|
||||
Prime(ctx context.Context, key Key, value interface{}) Interface
|
||||
}
|
||||
|
||||
// BatchFunc is a function, which when given a slice of keys (string), returns a slice of `results`.
|
||||
// It's important that the length of the input keys matches the length of the output results.
|
||||
//
|
||||
// The keys passed to this function are guaranteed to be unique
|
||||
type BatchFunc func(context.Context, Keys) []*Result
|
||||
|
||||
// Result is the data structure that a BatchFunc returns.
|
||||
// It contains the resolved data, and any errors that may have occurred while fetching the data.
|
||||
type Result struct {
|
||||
Data interface{}
|
||||
Error error
|
||||
}
|
||||
|
||||
// ResultMany is used by the LoadMany method.
|
||||
// It contains a list of resolved data and a list of errors.
|
||||
// The lengths of the data list and error list will match, and elements at each index correspond to each other.
|
||||
type ResultMany struct {
|
||||
Data []interface{}
|
||||
Error []error
|
||||
}
|
||||
|
||||
// Loader implements the dataloader.Interface.
|
||||
type Loader struct {
|
||||
// the batch function to be used by this loader
|
||||
batchFn BatchFunc
|
||||
|
||||
// the maximum batch size. Set to 0 if you want it to be unbounded.
|
||||
batchCap int
|
||||
|
||||
// the internal cache. This packages contains a basic cache implementation but any custom cache
|
||||
// implementation could be used as long as it implements the `Cache` interface.
|
||||
cacheLock sync.Mutex
|
||||
cache Cache
|
||||
// should we clear the cache on each batch?
|
||||
// this would allow batching but no long term caching
|
||||
clearCacheOnBatch bool
|
||||
|
||||
// count of queued up items
|
||||
count int
|
||||
|
||||
// the maximum input queue size. Set to 0 if you want it to be unbounded.
|
||||
inputCap int
|
||||
|
||||
// the amount of time to wait before triggering a batch
|
||||
wait time.Duration
|
||||
|
||||
// lock to protect the batching operations
|
||||
batchLock sync.Mutex
|
||||
|
||||
// current batcher
|
||||
curBatcher *batcher
|
||||
|
||||
// used to close the sleeper of the current batcher
|
||||
endSleeper chan bool
|
||||
|
||||
// used by tests to prevent logs
|
||||
silent bool
|
||||
|
||||
// can be set to trace calls to dataloader
|
||||
tracer Tracer
|
||||
}
|
||||
|
||||
// Thunk is a function that will block until the value (*Result) it contains is resolved.
|
||||
// After the value it contains is resolved, this function will return the result.
|
||||
// This function can be called many times, much like a Promise is other languages.
|
||||
// The value will only need to be resolved once so subsequent calls will return immediately.
|
||||
type Thunk func() (interface{}, error)
|
||||
|
||||
// ThunkMany is much like the Thunk func type but it contains a list of results.
|
||||
type ThunkMany func() ([]interface{}, []error)
|
||||
|
||||
// type used to on input channel
|
||||
type batchRequest struct {
|
||||
key Key
|
||||
channel chan *Result
|
||||
}
|
||||
|
||||
// Option allows for configuration of Loader fields.
|
||||
type Option func(*Loader)
|
||||
|
||||
// WithCache sets the BatchedLoader cache. Defaults to InMemoryCache if a Cache is not set.
|
||||
func WithCache(c Cache) Option {
|
||||
return func(l *Loader) {
|
||||
l.cache = c
|
||||
}
|
||||
}
|
||||
|
||||
// WithBatchCapacity sets the batch capacity. Default is 0 (unbounded).
|
||||
func WithBatchCapacity(c int) Option {
|
||||
return func(l *Loader) {
|
||||
l.batchCap = c
|
||||
}
|
||||
}
|
||||
|
||||
// WithInputCapacity sets the input capacity. Default is 1000.
|
||||
func WithInputCapacity(c int) Option {
|
||||
return func(l *Loader) {
|
||||
l.inputCap = c
|
||||
}
|
||||
}
|
||||
|
||||
// WithWait sets the amount of time to wait before triggering a batch.
|
||||
// Default duration is 16 milliseconds.
|
||||
func WithWait(d time.Duration) Option {
|
||||
return func(l *Loader) {
|
||||
l.wait = d
|
||||
}
|
||||
}
|
||||
|
||||
// WithClearCacheOnBatch allows batching of items but no long term caching.
|
||||
// It accomplishes this by clearing the cache after each batch operation.
|
||||
func WithClearCacheOnBatch() Option {
|
||||
return func(l *Loader) {
|
||||
l.cacheLock.Lock()
|
||||
l.clearCacheOnBatch = true
|
||||
l.cacheLock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// withSilentLogger turns of log messages. It's used by the tests
|
||||
func withSilentLogger() Option {
|
||||
return func(l *Loader) {
|
||||
l.silent = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithTracer allows tracing of calls to Load and LoadMany
|
||||
func WithTracer(tracer Tracer) Option {
|
||||
return func(l *Loader) {
|
||||
l.tracer = tracer
|
||||
}
|
||||
}
|
||||
|
||||
// WithOpenTracingTracer allows tracing of calls to Load and LoadMany
|
||||
func WithOpenTracingTracer() Option {
|
||||
return WithTracer(&OpenTracingTracer{})
|
||||
}
|
||||
|
||||
// NewBatchedLoader constructs a new Loader with given options.
|
||||
func NewBatchedLoader(batchFn BatchFunc, opts ...Option) *Loader {
|
||||
loader := &Loader{
|
||||
batchFn: batchFn,
|
||||
inputCap: 1000,
|
||||
wait: 16 * time.Millisecond,
|
||||
}
|
||||
|
||||
// Apply options
|
||||
for _, apply := range opts {
|
||||
apply(loader)
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
if loader.cache == nil {
|
||||
loader.cache = NewCache()
|
||||
}
|
||||
|
||||
if loader.tracer == nil {
|
||||
loader.tracer = &NoopTracer{}
|
||||
}
|
||||
|
||||
return loader
|
||||
}
|
||||
|
||||
// Load load/resolves the given key, returning a channel that will contain the value and error
|
||||
func (l *Loader) Load(originalContext context.Context, key Key) Thunk {
|
||||
ctx, finish := l.tracer.TraceLoad(originalContext, key)
|
||||
|
||||
c := make(chan *Result, 1)
|
||||
var result struct {
|
||||
mu sync.RWMutex
|
||||
value *Result
|
||||
}
|
||||
|
||||
// lock to prevent duplicate keys coming in before item has been added to cache.
|
||||
l.cacheLock.Lock()
|
||||
if v, ok := l.cache.Get(ctx, key); ok {
|
||||
defer finish(v)
|
||||
defer l.cacheLock.Unlock()
|
||||
return v
|
||||
}
|
||||
|
||||
thunk := func() (interface{}, error) {
|
||||
result.mu.RLock()
|
||||
resultNotSet := result.value == nil
|
||||
result.mu.RUnlock()
|
||||
|
||||
if resultNotSet {
|
||||
result.mu.Lock()
|
||||
if v, ok := <-c; ok {
|
||||
result.value = v
|
||||
}
|
||||
result.mu.Unlock()
|
||||
}
|
||||
result.mu.RLock()
|
||||
defer result.mu.RUnlock()
|
||||
return result.value.Data, result.value.Error
|
||||
}
|
||||
defer finish(thunk)
|
||||
|
||||
l.cache.Set(ctx, key, thunk)
|
||||
l.cacheLock.Unlock()
|
||||
|
||||
// this is sent to batch fn. It contains the key and the channel to return the
|
||||
// the result on
|
||||
req := &batchRequest{key, c}
|
||||
|
||||
l.batchLock.Lock()
|
||||
// start the batch window if it hasn't already started.
|
||||
if l.curBatcher == nil {
|
||||
l.curBatcher = l.newBatcher(l.silent, l.tracer)
|
||||
// start the current batcher batch function
|
||||
go l.curBatcher.batch(originalContext)
|
||||
// start a sleeper for the current batcher
|
||||
l.endSleeper = make(chan bool)
|
||||
go l.sleeper(l.curBatcher, l.endSleeper)
|
||||
}
|
||||
|
||||
l.curBatcher.input <- req
|
||||
|
||||
// if we need to keep track of the count (max batch), then do so.
|
||||
if l.batchCap > 0 {
|
||||
l.count++
|
||||
// if we hit our limit, force the batch to start
|
||||
if l.count == l.batchCap {
|
||||
// end the batcher synchronously here because another call to Load
|
||||
// may concurrently happen and needs to go to a new batcher.
|
||||
l.curBatcher.end()
|
||||
// end the sleeper for the current batcher.
|
||||
// this is to stop the goroutine without waiting for the
|
||||
// sleeper timeout.
|
||||
close(l.endSleeper)
|
||||
l.reset()
|
||||
}
|
||||
}
|
||||
l.batchLock.Unlock()
|
||||
|
||||
return thunk
|
||||
}
|
||||
|
||||
// LoadMany loads mulitiple keys, returning a thunk (type: ThunkMany) that will resolve the keys passed in.
|
||||
func (l *Loader) LoadMany(originalContext context.Context, keys Keys) ThunkMany {
|
||||
ctx, finish := l.tracer.TraceLoadMany(originalContext, keys)
|
||||
|
||||
var (
|
||||
length = len(keys)
|
||||
data = make([]interface{}, length)
|
||||
errors = make([]error, length)
|
||||
c = make(chan *ResultMany, 1)
|
||||
wg sync.WaitGroup
|
||||
)
|
||||
|
||||
resolve := func(ctx context.Context, i int) {
|
||||
defer wg.Done()
|
||||
thunk := l.Load(ctx, keys[i])
|
||||
result, err := thunk()
|
||||
data[i] = result
|
||||
errors[i] = err
|
||||
}
|
||||
|
||||
wg.Add(length)
|
||||
for i := range keys {
|
||||
go resolve(ctx, i)
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
|
||||
// errs is nil unless there exists a non-nil error.
|
||||
// This prevents dataloader from returning a slice of all-nil errors.
|
||||
var errs []error
|
||||
for _, e := range errors {
|
||||
if e != nil {
|
||||
errs = errors
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
c <- &ResultMany{Data: data, Error: errs}
|
||||
close(c)
|
||||
}()
|
||||
|
||||
var result struct {
|
||||
mu sync.RWMutex
|
||||
value *ResultMany
|
||||
}
|
||||
|
||||
thunkMany := func() ([]interface{}, []error) {
|
||||
result.mu.RLock()
|
||||
resultNotSet := result.value == nil
|
||||
result.mu.RUnlock()
|
||||
|
||||
if resultNotSet {
|
||||
result.mu.Lock()
|
||||
if v, ok := <-c; ok {
|
||||
result.value = v
|
||||
}
|
||||
result.mu.Unlock()
|
||||
}
|
||||
result.mu.RLock()
|
||||
defer result.mu.RUnlock()
|
||||
return result.value.Data, result.value.Error
|
||||
}
|
||||
|
||||
defer finish(thunkMany)
|
||||
return thunkMany
|
||||
}
|
||||
|
||||
// Clear clears the value at `key` from the cache, it it exsits. Returs self for method chaining
|
||||
func (l *Loader) Clear(ctx context.Context, key Key) Interface {
|
||||
l.cacheLock.Lock()
|
||||
l.cache.Delete(ctx, key)
|
||||
l.cacheLock.Unlock()
|
||||
return l
|
||||
}
|
||||
|
||||
// ClearAll clears the entire cache. To be used when some event results in unknown invalidations.
|
||||
// Returns self for method chaining.
|
||||
func (l *Loader) ClearAll() Interface {
|
||||
l.cacheLock.Lock()
|
||||
l.cache.Clear()
|
||||
l.cacheLock.Unlock()
|
||||
return l
|
||||
}
|
||||
|
||||
// Prime adds the provided key and value to the cache. If the key already exists, no change is made.
|
||||
// Returns self for method chaining
|
||||
func (l *Loader) Prime(ctx context.Context, key Key, value interface{}) Interface {
|
||||
if _, ok := l.cache.Get(ctx, key); !ok {
|
||||
thunk := func() (interface{}, error) {
|
||||
return value, nil
|
||||
}
|
||||
l.cache.Set(ctx, key, thunk)
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *Loader) reset() {
|
||||
l.count = 0
|
||||
l.curBatcher = nil
|
||||
|
||||
if l.clearCacheOnBatch {
|
||||
l.cache.Clear()
|
||||
}
|
||||
}
|
||||
|
||||
type batcher struct {
|
||||
input chan *batchRequest
|
||||
batchFn BatchFunc
|
||||
finished bool
|
||||
silent bool
|
||||
tracer Tracer
|
||||
}
|
||||
|
||||
// newBatcher returns a batcher for the current requests
|
||||
// all the batcher methods must be protected by a global batchLock
|
||||
func (l *Loader) newBatcher(silent bool, tracer Tracer) *batcher {
|
||||
return &batcher{
|
||||
input: make(chan *batchRequest, l.inputCap),
|
||||
batchFn: l.batchFn,
|
||||
silent: silent,
|
||||
tracer: tracer,
|
||||
}
|
||||
}
|
||||
|
||||
// stop receiving input and process batch function
|
||||
func (b *batcher) end() {
|
||||
if !b.finished {
|
||||
close(b.input)
|
||||
b.finished = true
|
||||
}
|
||||
}
|
||||
|
||||
// execute the batch of all items in queue
|
||||
func (b *batcher) batch(originalContext context.Context) {
|
||||
var (
|
||||
keys = make(Keys, 0)
|
||||
reqs = make([]*batchRequest, 0)
|
||||
items = make([]*Result, 0)
|
||||
panicErr interface{}
|
||||
)
|
||||
|
||||
for item := range b.input {
|
||||
keys = append(keys, item.key)
|
||||
reqs = append(reqs, item)
|
||||
}
|
||||
|
||||
ctx, finish := b.tracer.TraceBatch(originalContext, keys)
|
||||
defer finish(items)
|
||||
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
panicErr = r
|
||||
if b.silent {
|
||||
return
|
||||
}
|
||||
const size = 64 << 10
|
||||
buf := make([]byte, size)
|
||||
buf = buf[:runtime.Stack(buf, false)]
|
||||
log.Printf("Dataloader: Panic received in batch function: %v\n%s", panicErr, buf)
|
||||
}
|
||||
}()
|
||||
items = b.batchFn(ctx, keys)
|
||||
}()
|
||||
|
||||
if panicErr != nil {
|
||||
for _, req := range reqs {
|
||||
req.channel <- &Result{Error: fmt.Errorf("Panic received in batch function: %v", panicErr)}
|
||||
close(req.channel)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if len(items) != len(keys) {
|
||||
err := &Result{Error: fmt.Errorf(`
|
||||
The batch function supplied did not return an array of responses
|
||||
the same length as the array of keys.
|
||||
|
||||
Keys:
|
||||
%v
|
||||
|
||||
Values:
|
||||
%v
|
||||
`, keys, items)}
|
||||
|
||||
for _, req := range reqs {
|
||||
req.channel <- err
|
||||
close(req.channel)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for i, req := range reqs {
|
||||
req.channel <- items[i]
|
||||
close(req.channel)
|
||||
}
|
||||
}
|
||||
|
||||
// wait the appropriate amount of time for the provided batcher
|
||||
func (l *Loader) sleeper(b *batcher, close chan bool) {
|
||||
select {
|
||||
// used by batch to close early. usually triggered by max batch size
|
||||
case <-close:
|
||||
return
|
||||
// this will move this goroutine to the back of the callstack?
|
||||
case <-time.After(l.wait):
|
||||
}
|
||||
|
||||
// reset
|
||||
// this is protected by the batchLock to avoid closing the batcher input
|
||||
// channel while Load is inserting a request
|
||||
l.batchLock.Lock()
|
||||
b.end()
|
||||
|
||||
// We can end here also if the batcher has already been closed and a
|
||||
// new one has been created. So reset the loader state only if the batcher
|
||||
// is the current one
|
||||
if l.curBatcher == b {
|
||||
l.reset()
|
||||
}
|
||||
l.batchLock.Unlock()
|
||||
}
|
||||
10
vendor/github.com/graph-gophers/dataloader/v6/go.mod
сгенерированный
поставляемый
Обычный файл
10
vendor/github.com/graph-gophers/dataloader/v6/go.mod
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,10 @@
|
||||
module github.com/graph-gophers/dataloader/v6
|
||||
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/hashicorp/golang-lru v0.5.4
|
||||
github.com/opentracing/opentracing-go v1.2.0
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/stretchr/testify v1.6.1 // indirect
|
||||
)
|
||||
18
vendor/github.com/graph-gophers/dataloader/v6/go.sum
сгенерированный
поставляемый
Обычный файл
18
vendor/github.com/graph-gophers/dataloader/v6/go.sum
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,18 @@
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
|
||||
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
63
vendor/github.com/graph-gophers/dataloader/v6/in_memory_cache.go
сгенерированный
поставляемый
Обычный файл
63
vendor/github.com/graph-gophers/dataloader/v6/in_memory_cache.go
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,63 @@
|
||||
package dataloader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// InMemoryCache is an in memory implementation of Cache interface.
|
||||
// This simple implementation is well suited for
|
||||
// a "per-request" dataloader (i.e. one that only lives
|
||||
// for the life of an http request) but it's not well suited
|
||||
// for long lived cached items.
|
||||
type InMemoryCache struct {
|
||||
items map[string]Thunk
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewCache constructs a new InMemoryCache
|
||||
func NewCache() *InMemoryCache {
|
||||
items := make(map[string]Thunk)
|
||||
return &InMemoryCache{
|
||||
items: items,
|
||||
}
|
||||
}
|
||||
|
||||
// Set sets the `value` at `key` in the cache
|
||||
func (c *InMemoryCache) Set(_ context.Context, key Key, value Thunk) {
|
||||
c.mu.Lock()
|
||||
c.items[key.String()] = value
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// Get gets the value at `key` if it exsits, returns value (or nil) and bool
|
||||
// indicating of value was found
|
||||
func (c *InMemoryCache) Get(_ context.Context, key Key) (Thunk, bool) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
item, found := c.items[key.String()]
|
||||
if !found {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return item, true
|
||||
}
|
||||
|
||||
// Delete deletes item at `key` from cache
|
||||
func (c *InMemoryCache) Delete(ctx context.Context, key Key) bool {
|
||||
if _, found := c.Get(ctx, key); found {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
delete(c.items, key.String())
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Clear clears the entire cache
|
||||
func (c *InMemoryCache) Clear() {
|
||||
c.mu.Lock()
|
||||
c.items = map[string]Thunk{}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
39
vendor/github.com/graph-gophers/dataloader/v6/key.go
сгенерированный
поставляемый
Обычный файл
39
vendor/github.com/graph-gophers/dataloader/v6/key.go
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,39 @@
|
||||
package dataloader
|
||||
|
||||
// Key is the interface that all keys need to implement
|
||||
type Key interface {
|
||||
// String returns a guaranteed unique string that can be used to identify an object
|
||||
String() string
|
||||
// Raw returns the raw, underlaying value of the key
|
||||
Raw() interface{}
|
||||
}
|
||||
|
||||
// Keys wraps a slice of Key types to provide some convenience methods.
|
||||
type Keys []Key
|
||||
|
||||
// Keys returns the list of strings. One for each "Key" in the list
|
||||
func (l Keys) Keys() []string {
|
||||
list := make([]string, len(l))
|
||||
for i := range l {
|
||||
list[i] = l[i].String()
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// StringKey implements the Key interface for a string
|
||||
type StringKey string
|
||||
|
||||
// String is an identity method. Used to implement String interface
|
||||
func (k StringKey) String() string { return string(k) }
|
||||
|
||||
// Raw is an identity method. Used to implement Key Raw
|
||||
func (k StringKey) Raw() interface{} { return k }
|
||||
|
||||
// NewKeysFromStrings converts a `[]strings` to a `Keys` ([]Key)
|
||||
func NewKeysFromStrings(strings []string) Keys {
|
||||
list := make(Keys, len(strings))
|
||||
for i := range strings {
|
||||
list[i] = StringKey(strings[i])
|
||||
}
|
||||
return list
|
||||
}
|
||||
78
vendor/github.com/graph-gophers/dataloader/v6/trace.go
сгенерированный
поставляемый
Обычный файл
78
vendor/github.com/graph-gophers/dataloader/v6/trace.go
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,78 @@
|
||||
package dataloader
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
)
|
||||
|
||||
type TraceLoadFinishFunc func(Thunk)
|
||||
type TraceLoadManyFinishFunc func(ThunkMany)
|
||||
type TraceBatchFinishFunc func([]*Result)
|
||||
|
||||
// Tracer is an interface that may be used to implement tracing.
|
||||
type Tracer interface {
|
||||
// TraceLoad will trace the calls to Load
|
||||
TraceLoad(ctx context.Context, key Key) (context.Context, TraceLoadFinishFunc)
|
||||
// TraceLoadMany will trace the calls to LoadMany
|
||||
TraceLoadMany(ctx context.Context, keys Keys) (context.Context, TraceLoadManyFinishFunc)
|
||||
// TraceBatch will trace data loader batches
|
||||
TraceBatch(ctx context.Context, keys Keys) (context.Context, TraceBatchFinishFunc)
|
||||
}
|
||||
|
||||
// OpenTracing Tracer implements a tracer that can be used with the Open Tracing standard.
|
||||
type OpenTracingTracer struct{}
|
||||
|
||||
// TraceLoad will trace a call to dataloader.LoadMany with Open Tracing
|
||||
func (OpenTracingTracer) TraceLoad(ctx context.Context, key Key) (context.Context, TraceLoadFinishFunc) {
|
||||
span, spanCtx := opentracing.StartSpanFromContext(ctx, "Dataloader: load")
|
||||
|
||||
span.SetTag("dataloader.key", key.String())
|
||||
|
||||
return spanCtx, func(thunk Thunk) {
|
||||
// TODO: is there anything we should do with the results?
|
||||
span.Finish()
|
||||
}
|
||||
}
|
||||
|
||||
// TraceLoadMany will trace a call to dataloader.LoadMany with Open Tracing
|
||||
func (OpenTracingTracer) TraceLoadMany(ctx context.Context, keys Keys) (context.Context, TraceLoadManyFinishFunc) {
|
||||
span, spanCtx := opentracing.StartSpanFromContext(ctx, "Dataloader: loadmany")
|
||||
|
||||
span.SetTag("dataloader.keys", keys.Keys())
|
||||
|
||||
return spanCtx, func(thunk ThunkMany) {
|
||||
// TODO: is there anything we should do with the results?
|
||||
span.Finish()
|
||||
}
|
||||
}
|
||||
|
||||
// TraceBatch will trace a call to dataloader.LoadMany with Open Tracing
|
||||
func (OpenTracingTracer) TraceBatch(ctx context.Context, keys Keys) (context.Context, TraceBatchFinishFunc) {
|
||||
span, spanCtx := opentracing.StartSpanFromContext(ctx, "Dataloader: batch")
|
||||
|
||||
span.SetTag("dataloader.keys", keys.Keys())
|
||||
|
||||
return spanCtx, func(results []*Result) {
|
||||
// TODO: is there anything we should do with the results?
|
||||
span.Finish()
|
||||
}
|
||||
}
|
||||
|
||||
// NoopTracer is the default (noop) tracer
|
||||
type NoopTracer struct{}
|
||||
|
||||
// TraceLoad is a noop function
|
||||
func (NoopTracer) TraceLoad(ctx context.Context, key Key) (context.Context, TraceLoadFinishFunc) {
|
||||
return ctx, func(Thunk) {}
|
||||
}
|
||||
|
||||
// TraceLoadMany is a noop function
|
||||
func (NoopTracer) TraceLoadMany(ctx context.Context, keys Keys) (context.Context, TraceLoadManyFinishFunc) {
|
||||
return ctx, func(ThunkMany) {}
|
||||
}
|
||||
|
||||
// TraceBatch is a noop function
|
||||
func (NoopTracer) TraceBatch(ctx context.Context, keys Keys) (context.Context, TraceBatchFinishFunc) {
|
||||
return ctx, func(result []*Result) {}
|
||||
}
|
||||
4
vendor/modules.txt
поставляемый
4
vendor/modules.txt
поставляемый
@@ -276,6 +276,9 @@ github.com/gorilla/schema
|
||||
# github.com/gorilla/websocket v1.5.0
|
||||
## explicit
|
||||
github.com/gorilla/websocket
|
||||
# github.com/graph-gophers/dataloader/v6 v6.0.0
|
||||
## explicit
|
||||
github.com/graph-gophers/dataloader/v6
|
||||
# github.com/graph-gophers/graphql-go v1.3.0
|
||||
## explicit
|
||||
github.com/graph-gophers/graphql-go
|
||||
@@ -322,7 +325,6 @@ github.com/hashicorp/go-plugin/internal/plugin
|
||||
## explicit
|
||||
github.com/hashicorp/go-sockaddr
|
||||
# github.com/hashicorp/golang-lru v0.5.4
|
||||
## explicit
|
||||
github.com/hashicorp/golang-lru
|
||||
github.com/hashicorp/golang-lru/simplelru
|
||||
# github.com/hashicorp/memberlist v0.3.1
|
||||
|
||||
Ссылка в новой задаче
Block a user