/
operation_planner.go
145 lines (124 loc) · 4.37 KB
/
operation_planner.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
package core
import (
"errors"
"strconv"
"github.com/wundergraph/graphql-go-tools/v2/pkg/ast"
"github.com/wundergraph/graphql-go-tools/v2/pkg/astparser"
"github.com/wundergraph/graphql-go-tools/v2/pkg/astvalidation"
"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/plan"
"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/postprocess"
"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve"
"golang.org/x/sync/singleflight"
"github.com/wundergraph/cosmo/router/internal/unsafebytes"
)
type planWithMetaData struct {
preparedPlan plan.Plan
operationDocument, schemaDocument *ast.Document
}
type OperationPlanner struct {
sf singleflight.Group
planCache ExecutionPlanCache
executor *Executor
}
type ExecutionPlanCache interface {
Get(key interface{}) (interface{}, bool)
Set(key, value interface{}, cost int64) bool
}
func NewNoopExecutionPlanCache() ExecutionPlanCache {
return &noopExecutionPlanCache{}
}
type noopExecutionPlanCache struct{}
func (n *noopExecutionPlanCache) Get(key interface{}) (interface{}, bool) {
return nil, false
}
func (n *noopExecutionPlanCache) Set(key, value interface{}, cost int64) bool {
return true
}
func NewOperationPlanner(executor *Executor, planCache ExecutionPlanCache) *OperationPlanner {
return &OperationPlanner{
planCache: planCache,
executor: executor,
}
}
func (p *OperationPlanner) preparePlan(requestOperationName []byte, requestOperationContent string) (*planWithMetaData, error) {
doc, report := astparser.ParseGraphqlDocumentString(requestOperationContent)
if report.HasErrors() {
return nil, &reportError{report: &report}
}
validation := astvalidation.DefaultOperationValidator()
// validate the document before planning
state := validation.Validate(&doc, p.executor.Definition, &report)
if state != astvalidation.Valid {
return nil, &reportError{report: &report}
}
planner, err := plan.NewPlanner(p.executor.PlanConfig)
if err != nil {
return nil, err
}
// create and postprocess the plan
preparedPlan := planner.Plan(&doc, p.executor.Definition, unsafebytes.BytesToString(requestOperationName), &report)
if report.HasErrors() {
return nil, &reportError{report: &report}
}
post := postprocess.DefaultProcessor()
post.Process(preparedPlan)
return &planWithMetaData{
preparedPlan: preparedPlan,
operationDocument: &doc,
schemaDocument: p.executor.Definition,
}, nil
}
func (p *OperationPlanner) Plan(operation *ParsedOperation, clientInfo *ClientInfo, protocol OperationProtocol, traceOptions resolve.TraceOptions) (*operationContext, error) {
opContext := &operationContext{
name: operation.Name,
opType: operation.Type,
content: operation.NormalizedRepresentation,
hash: operation.ID,
clientInfo: clientInfo,
variables: operation.Variables,
traceOptions: traceOptions,
extensions: operation.Extensions,
persistedID: operation.PersistedID,
protocol: protocol,
}
if traceOptions.Enable {
// if we have tracing enabled we always prepare a new plan
// this is because we're writing trace data to the plan
requestOperationNameBytes := unsafebytes.StringToBytes(opContext.Name())
prepared, err := p.preparePlan(requestOperationNameBytes, opContext.Content())
if err != nil {
return nil, err
}
opContext.preparedPlan = prepared
return opContext, nil
}
operationID := opContext.Hash()
// try to get a prepared plan for this operation ID from the cache
cachedPlan, ok := p.planCache.Get(operationID)
if ok && cachedPlan != nil {
// re-use a prepared plan
opContext.preparedPlan = cachedPlan.(*planWithMetaData)
opContext.planCacheHit = true
} else {
// prepare a new plan using single flight
// this ensures that we only prepare the plan once for this operation ID
operationIDStr := strconv.FormatUint(operationID, 10)
sharedPreparedPlan, err, _ := p.sf.Do(operationIDStr, func() (interface{}, error) {
requestOperationNameBytes := unsafebytes.StringToBytes(opContext.Name())
prepared, err := p.preparePlan(requestOperationNameBytes, opContext.Content())
if err != nil {
return nil, err
}
p.planCache.Set(operationID, prepared, 1)
return prepared, nil
})
if err != nil {
return nil, err
}
opContext.preparedPlan, ok = sharedPreparedPlan.(*planWithMetaData)
if !ok {
return nil, errors.New("unexpected prepared plan type")
}
}
return opContext, nil
}