Skip to content

Commit 8d0aad8

Browse files
Refactor field resolution closer to schema types (#573)
* Refactor field resolution closer to schema types Much of the work of executing resolvers relates to the resolvable portion of fields, rather than the selection made for the request. With the introduction of directive visitors, these were being recreated for each request, instead of being built once, and re-used. By pushing the resolution of fields down into the `resolvable.Field`, directives applied to the field definition can be setup just once when the schema is created, and re-used across all requests. Small exceptions to this exist for field types that embed this to provide request specific values (resolver instance and arguments), which get passed along with the call. The chain of directive visitor functions still needs to be recreated for each resolve call, because the inner resolve function needs to accept the request specific field resolver instance, but other per-request overheads should be removed. * Fix feedback --------- Co-authored-by: Pavel Nikolov <[email protected]>
1 parent 28df463 commit 8d0aad8

File tree

3 files changed

+166
-146
lines changed

3 files changed

+166
-146
lines changed

internal/exec/exec.go

Lines changed: 1 addition & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,6 @@ func (r *Request) Execute(ctx context.Context, s *resolvable.Schema, op *types.O
6464
return out.Bytes(), r.Errs
6565
}
6666

67-
type resolverFunc func(ctx context.Context, args interface{}) (output interface{}, err error)
68-
69-
func (f resolverFunc) Resolve(ctx context.Context, args interface{}) (output interface{}, err error) {
70-
return f(ctx, args)
71-
}
72-
7367
type fieldToExec struct {
7468
field *selected.SchemaField
7569
sels []selected.Selection
@@ -78,58 +72,7 @@ type fieldToExec struct {
7872
}
7973

8074
func (f *fieldToExec) resolve(ctx context.Context) (output interface{}, err error) {
81-
var args interface{}
82-
83-
if f.field.ArgsPacker != nil {
84-
args = f.field.PackedArgs.Interface()
85-
}
86-
87-
currResolver := f.resolveField
88-
89-
for _, pd := range f.field.PackedDirectives {
90-
pd := pd // Needed to avoid passing only the last directive, since we're closing over this loop var pointer
91-
innerResolver := currResolver
92-
93-
currResolver = func(ctx context.Context, args interface{}) (output interface{}, err error) {
94-
return pd.Resolve(ctx, args, resolverFunc(innerResolver))
95-
}
96-
}
97-
98-
return currResolver(ctx, args)
99-
}
100-
101-
func (f *fieldToExec) resolveField(ctx context.Context, args interface{}) (output interface{}, err error) {
102-
if !f.field.UseMethodResolver() {
103-
res := f.resolver
104-
105-
// TODO extract out unwrapping ptr logic to a common place
106-
if res.Kind() == reflect.Ptr {
107-
res = res.Elem()
108-
}
109-
110-
return res.FieldByIndex(f.field.FieldIndex).Interface(), nil
111-
}
112-
113-
var in []reflect.Value
114-
var callOut []reflect.Value
115-
116-
if f.field.HasContext {
117-
in = append(in, reflect.ValueOf(ctx))
118-
}
119-
120-
if f.field.ArgsPacker != nil {
121-
in = append(in, reflect.ValueOf(args))
122-
}
123-
124-
callOut = f.resolver.Method(f.field.MethodIndex).Call(in)
125-
result := callOut[0]
126-
127-
if f.field.HasError && !callOut[1].IsNil() {
128-
resolverErr := callOut[1].Interface().(error)
129-
return result.Interface(), resolverErr
130-
}
131-
132-
return result.Interface(), nil
75+
return f.field.Resolve(ctx, f.resolver)
13376
}
13477

13578
func resolvedToNull(b *bytes.Buffer) bool {

internal/exec/resolvable/resolvable.go

Lines changed: 142 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ type Field struct {
4747
HasContext bool
4848
HasError bool
4949
ArgsPacker *packer.StructPacker
50-
DirectivesPackers map[string]*packer.StructPacker
50+
DirectiveVisitors []directives.ResolverInterceptor
5151
ValueExec Resolvable
5252
TraceLabel string
5353
}
@@ -56,6 +56,68 @@ func (f *Field) UseMethodResolver() bool {
5656
return len(f.FieldIndex) == 0
5757
}
5858

59+
func (f *Field) Resolve(ctx context.Context, resolver reflect.Value, args interface{}) (output interface{}, err error) {
60+
// Short circuit case to avoid wrapping functions
61+
if len(f.DirectiveVisitors) == 0 {
62+
return f.resolve(ctx, resolver, args)
63+
}
64+
65+
wrapResolver := func(ctx context.Context, args interface{}) (output interface{}, err error) {
66+
return f.resolve(ctx, resolver, args)
67+
}
68+
69+
for _, d := range f.DirectiveVisitors {
70+
d := d // Needed to avoid passing only the last directive, since we're closing over this loop var pointer
71+
innerResolver := wrapResolver
72+
73+
wrapResolver = func(ctx context.Context, args interface{}) (output interface{}, err error) {
74+
return d.Resolve(ctx, args, resolverFunc(innerResolver))
75+
}
76+
}
77+
78+
return wrapResolver(ctx, args)
79+
}
80+
81+
func (f *Field) resolve(ctx context.Context, resolver reflect.Value, args interface{}) (output interface{}, err error) {
82+
if !f.UseMethodResolver() {
83+
res := resolver
84+
85+
// TODO extract out unwrapping ptr logic to a common place
86+
if res.Kind() == reflect.Ptr {
87+
res = res.Elem()
88+
}
89+
90+
return res.FieldByIndex(f.FieldIndex).Interface(), nil
91+
}
92+
93+
var in []reflect.Value
94+
var callOut []reflect.Value
95+
96+
if f.HasContext {
97+
in = append(in, reflect.ValueOf(ctx))
98+
}
99+
100+
if f.ArgsPacker != nil {
101+
in = append(in, reflect.ValueOf(args))
102+
}
103+
104+
callOut = resolver.Method(f.MethodIndex).Call(in)
105+
result := callOut[0]
106+
107+
if f.HasError && !callOut[1].IsNil() {
108+
resolverErr := callOut[1].Interface().(error)
109+
return result.Interface(), resolverErr
110+
}
111+
112+
return result.Interface(), nil
113+
}
114+
115+
type resolverFunc func(ctx context.Context, args interface{}) (output interface{}, err error)
116+
117+
func (f resolverFunc) Resolve(ctx context.Context, args interface{}) (output interface{}, err error) {
118+
return f(ctx, args)
119+
}
120+
59121
type TypeAssertion struct {
60122
MethodIndex int
61123
TypeExec Resolvable
@@ -71,17 +133,22 @@ func (*Object) isResolvable() {}
71133
func (*List) isResolvable() {}
72134
func (*Scalar) isResolvable() {}
73135

74-
func ApplyResolver(s *types.Schema, resolver interface{}, dirVisitors []directives.Directive, useFieldResolvers bool) (*Schema, error) {
136+
func ApplyResolver(s *types.Schema, resolver interface{}, dirs []directives.Directive, useFieldResolvers bool) (*Schema, error) {
75137
if resolver == nil {
76138
return &Schema{Meta: newMeta(s), Schema: *s}, nil
77139
}
78140

79-
ds, err := applyDirectives(s, dirVisitors)
141+
ds, err := applyDirectives(s, dirs)
142+
if err != nil {
143+
return nil, err
144+
}
145+
146+
directivePackers, err := buildDirectivePackers(s, ds)
80147
if err != nil {
81148
return nil, err
82149
}
83150

84-
b := newBuilder(s, ds, useFieldResolvers)
151+
b := newBuilder(s, directivePackers, useFieldResolvers)
85152

86153
var query, mutation, subscription Resolvable
87154

@@ -158,6 +225,44 @@ func ApplyResolver(s *types.Schema, resolver interface{}, dirVisitors []directiv
158225
}, nil
159226
}
160227

228+
func buildDirectivePackers(s *types.Schema, visitors map[string]directives.Directive) (map[string]*packer.StructPacker, error) {
229+
// Directive packers need to use a dedicated builder which is ready ('finish()' called) while
230+
// schema fields (and their argument packers) are still being built
231+
builder := packer.NewBuilder()
232+
233+
packers := map[string]*packer.StructPacker{}
234+
for _, d := range s.Directives {
235+
n := d.Name
236+
237+
v, ok := visitors[n]
238+
if !ok {
239+
// Directives which need visitors have already been checked
240+
// Anything without a visitor now is a built-in directive without a packer.
241+
continue
242+
}
243+
244+
if _, ok = v.(directives.ResolverInterceptor); !ok {
245+
// Directive doesn't apply at field resolution time, skip it
246+
continue
247+
}
248+
249+
r := reflect.TypeOf(v)
250+
251+
p, err := builder.MakeStructPacker(d.Arguments, r)
252+
if err != nil {
253+
return nil, err
254+
}
255+
256+
packers[n] = p
257+
}
258+
259+
if err := builder.Finish(); err != nil {
260+
return nil, err
261+
}
262+
263+
return packers, nil
264+
}
265+
161266
func applyDirectives(s *types.Schema, visitors []directives.Directive) (map[string]directives.Directive, error) {
162267
byName := make(map[string]directives.Directive, len(s.Directives))
163268

@@ -209,7 +314,7 @@ func applyDirectives(s *types.Schema, visitors []directives.Directive) (map[stri
209314
type execBuilder struct {
210315
schema *types.Schema
211316
resMap map[typePair]*resMapEntry
212-
directives map[string]directives.Directive
317+
directivePackers map[string]*packer.StructPacker
213318
packerBuilder *packer.Builder
214319
useFieldResolvers bool
215320
}
@@ -224,11 +329,11 @@ type resMapEntry struct {
224329
targets []*Resolvable
225330
}
226331

227-
func newBuilder(s *types.Schema, directives map[string]directives.Directive, useFieldResolvers bool) *execBuilder {
332+
func newBuilder(s *types.Schema, directives map[string]*packer.StructPacker, useFieldResolvers bool) *execBuilder {
228333
return &execBuilder{
229334
schema: s,
230335
resMap: make(map[typePair]*resMapEntry),
231-
directives: directives,
336+
directivePackers: directives,
232337
packerBuilder: packer.NewBuilder(),
233338
useFieldResolvers: useFieldResolvers,
234339
}
@@ -471,39 +576,9 @@ func (b *execBuilder) makeFieldExec(typeName string, f *types.FieldDefinition, m
471576
}
472577
}
473578

474-
directivesPackers := map[string]*packer.StructPacker{}
475-
for _, d := range f.Directives {
476-
n := d.Name.Name
477-
478-
// skip special directives without packers
479-
if n == "deprecated" {
480-
continue
481-
}
482-
483-
v, ok := b.directives[n]
484-
if !ok {
485-
return nil, fmt.Errorf("directive %q on field %q does not have a visitor registered with the schema", n, f.Name)
486-
}
487-
488-
if _, ok = v.(directives.ResolverInterceptor); !ok {
489-
// Directive doesn't apply at field resolution time, skip it
490-
continue
491-
}
492-
493-
r := reflect.TypeOf(v)
494-
495-
// The directive definition is needed here in order to get the arguments definition list.
496-
// d.Arguments wouldn't work in this case because it does not contain args type information.
497-
dd, ok := b.schema.Directives[n]
498-
if !ok {
499-
return nil, fmt.Errorf("directive definition %q is not defined in the schema", n)
500-
}
501-
p, err := b.packerBuilder.MakeStructPacker(dd.Arguments, r)
502-
if err != nil {
503-
return nil, err
504-
}
505-
506-
directivesPackers[n] = p
579+
pd, err := packDirectives(f.Directives, b.directivePackers)
580+
if err != nil {
581+
return nil, err
507582
}
508583

509584
fe := &Field{
@@ -513,7 +588,7 @@ func (b *execBuilder) makeFieldExec(typeName string, f *types.FieldDefinition, m
513588
FieldIndex: fieldIndex,
514589
HasContext: hasContext,
515590
ArgsPacker: argsPacker,
516-
DirectivesPackers: directivesPackers,
591+
DirectiveVisitors: pd,
517592
HasError: hasError,
518593
TraceLabel: fmt.Sprintf("GraphQL field: %s.%s", typeName, f.Name),
519594
}
@@ -535,6 +610,32 @@ func (b *execBuilder) makeFieldExec(typeName string, f *types.FieldDefinition, m
535610
return fe, nil
536611
}
537612

613+
func packDirectives(ds types.DirectiveList, packers map[string]*packer.StructPacker) ([]directives.ResolverInterceptor, error) {
614+
packed := make([]directives.ResolverInterceptor, 0, len(ds))
615+
for _, d := range ds {
616+
dp, ok := packers[d.Name.Name]
617+
if !ok {
618+
continue // skip directives without packers
619+
}
620+
621+
args := make(map[string]interface{})
622+
for _, arg := range d.Arguments {
623+
args[arg.Name.Name] = arg.Value.Deserialize(nil)
624+
}
625+
626+
p, err := dp.Pack(args)
627+
if err != nil {
628+
return nil, err
629+
}
630+
631+
v := p.Interface().(directives.ResolverInterceptor)
632+
633+
packed = append(packed, v)
634+
}
635+
636+
return packed, nil
637+
}
638+
538639
func findMethod(t reflect.Type, name string) int {
539640
for i := 0; i < t.NumMethod(); i++ {
540641
if strings.EqualFold(stripUnderscore(name), stripUnderscore(t.Method(i).Name)) {

0 commit comments

Comments
 (0)