Skip to content

Commit

Permalink
Merge pull request #74 from movio/new-execution
Browse files Browse the repository at this point in the history
Rewritten execution pipeline
  • Loading branch information
Lucian Jones authored Oct 1, 2021
2 parents 93ae867 + ee3f3d8 commit 7927df3
Show file tree
Hide file tree
Showing 15 changed files with 4,136 additions and 1,474 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Bramble supports:
- Namespaces
- Field-level permissions
- Plugins:
- JWT, Open tracing, CORS, ...
- JWT, CORS, ...
- Or add your own
- Hot reloading of configuration

Expand Down
19 changes: 7 additions & 12 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,13 @@ import (
"strings"
"time"

opentracing "github.com/opentracing/opentracing-go"
"github.com/vektah/gqlparser/v2/ast"
)

// GraphQLClient is a GraphQL client.
type GraphQLClient struct {
HTTPClient *http.Client
MaxResponseSize int64
Tracer opentracing.Tracer
UserAgent string
}

Expand Down Expand Up @@ -82,16 +81,6 @@ func (c *GraphQLClient) Request(ctx context.Context, url string, request *Reques
httpReq.Header.Set("User-Agent", c.UserAgent)
}

if c.Tracer != nil {
span := opentracing.SpanFromContext(ctx)
if span != nil {
c.Tracer.Inject(
span.Context(),
opentracing.HTTPHeaders,
opentracing.HTTPHeadersCarrier(httpReq.Header))
}
}

res, err := c.HTTPClient.Do(httpReq)
if err != nil {
return fmt.Errorf("error during request: %w", err)
Expand Down Expand Up @@ -144,6 +133,11 @@ func NewRequest(body string) *Request {
}
}

func (r *Request) WithHeaders(headers http.Header) *Request {
r.Headers = headers
return r
}

// Response is a GraphQL response
type Response struct {
Errors GraphqlErrors `json:"errors"`
Expand All @@ -157,6 +151,7 @@ type GraphqlErrors []GraphqlError
// GraphqlError is a single GraphQL error
type GraphqlError struct {
Message string `json:"message"`
Path ast.Path `json:"path,omitempty"`
Extensions map[string]interface{} `json:"extensions"`
}

Expand Down
117 changes: 1 addition & 116 deletions docs/algorithms.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,119 +303,4 @@ function RouteSelectionSet(ctx, parentType, selectionSet) {

## Query Execution

The `Execute` function is straightforward, it simply iterates over each root step in the query plan, and executes them in turn. The implementation does this in parallel, but this is omitted in the pseudo-code for simplicity.

```
function Execute(ctx, queryPlan, resultPtr) {
for step in queryPlan.RootSteps {
ExecuteRootStep(ctx, step, resultPtr)
}
}
```

The `ExecuteRootStep` function executes a single step of the query plan, along with its children steps, if any.

The operation document is simply composed of the operation type and the step's selection set.

Once the document is constructed, we invoke the remote GraphQL service with the document and store the response at the given result pointer.

Finally, for each child step in `step.Then`, we call `ExecuteChildStep`.

```
function ExecuteRootStep(ctx, step, resultPtr) {
operationType = if step.ParentType == "Mutation" then "mutation" else "query"
if id is the empty string {
document = "${operationType} ${step.SelectionSet}"
}
execute document at URL step.ServiceURL and write response to resultPtr
for childStep in step.Then {
ExecuteChildStep(ctx, childStep, resultPtr)
}
}
```

The `ExecuteChildStep` function execute a single child step, along with its
children steps, if any.

First we build the corresponding insertion slice. This is a slice containing
all the target elements for the operation (where we need to insert the data).
They are represented by the id of the element along with a pointer to a
structure that can receive JSON document. See `buildInsertionSlice` below.

Then we build the document: one boundary query per insertion target. To avoid
conflict we alias each query with an id.

Once we have the document is constructed we invoke the remote GraphQL service
and store the response into each corresponding target.

Finally recursively call `ExecuteChildStep` for each child step in
`step.Then`.

```
function ExecuteChildStep(ctx, step, resultPtr) {
targets = buildInsertionSlice(step, resultPtr)
queries = []
for target in targets {
query = """
{
${id}: $boundaryQuery(id: ${target.Id}) {
${step.SelectionSet}
}
}
"""
append query to queries
}
document = "{ ${queries} }"
execute document at URL step.ServiceURL and write response to resultPtr
for childStep in step.Then {
ExecuteChildStep(ctx, childStep, resultPtr)
}
}
```

The `buildInsertionSlice` algorithm traverses the structure pointed by
`resultPtr`, along the path described by `insertionPoint`. It returns a slice
of pointers to JSON results along with the id of the element.
Those pointers indicate where data should be written by a step that has the
corresponding insertion point.

First, if the insertion point is empty, it means that we have reached the end of the path, and `resultPtr` points to the destination we were looking for. If this destination is a `map`, we return a singleton slice of that map. If this destination is a slice then we call `buildInsertionSlice` recursively on each element of that slice in order to ensure that the returned slice is not nested (`resultPtr` may be a list of lists, in which case the resulting slice must be flattened).

Finally, if the insertion point is not empty, we consider whether `resultPtr` is a map or a slice.
<br/>
If it's a map, we look up the insertion point's first item in that map and call `buildInsertionSlice` recursively on that value, passing a new insertion point to the recursive call that skips that first element.
<br/>
If `resultPtr` is a slice we perform the same operation as described above, i.e. we call `buildInsertionSlice` recursively on each element of that slice in order to ensure that the returned slice is not nested.

```
function buildInsertionSlice(insertionPoint, resultPtr) {
if insertionPoint is empty {
switch on the type of resultPtr {
case resultPtr is a slice:
newResultPtr = empty slice
for element in resultPtr {
for newElement in buildInsertionSlice(insertionPoint, element) {
append newElement to newResultPtr
}
}
return newResultPtr
case resultPtr is a map:
id = resultPtr["id"] || resultPtr["_id"]
return [ (id, resultPtr) ]
}
}
switch on the type of resultPtr {
case resultPtr is a slice:
newResultPtr = empty slice
for element in resultPtr {
for newElement in buildInsertionSlice(insertionPoint, element) {
append newElement to newResultPtr
}
}
return newResultPtr
case resultPtr is a map:
return buildInsertionSlice(insertionPoint[1:], resultPtr[insertionPoint[0]])
}
}
```
To be updated for the new execution pipeline.
69 changes: 0 additions & 69 deletions docs/debugging.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,73 +9,4 @@ One or multiple of the following options can be provided (white space separated)
- `query`: input query
- `plan`: the query plan, including services and subqueries
- `timing`: total execution time for the query (as a duration string, e.g. `12ms`)
- `trace-id`: the jaeger trace-id
- `all` (all of the above)

## Open tracing (Jaeger)

Tracing is a powerful way to understand exactly how your queries are executed and to troubleshoot slow queries.

### Enable tracing on Bramble

See the [open tracing plugin](plugins?id=open-tracing-jaeger).

### Add tracing to your services (optional)

Adding tracing to your individual services will add a lot more details to your traces.

1. Create a tracer, see the [Jaeger documentation](https://pkg.go.dev/github.com/uber/jaeger-client-go#NewTracer)

2. Add a tracing middleware to your HTTP endpoint.

```go
mux.Handle("/query", NewTracingMiddleware(tracer).Middleware(gqlserver))
```

<details>
<summary>
<strong>Example Go middleware</strong>
</summary>

```go
// TracingMiddleware is a middleware to add open tracing to incoming requests.
// It creates a span for each incoming requests, using the request context if
// present.
type TracingMiddleware struct {
tracer opentracing.Tracer
}

// NewTracingMiddleware returns a new tracing middleware
func NewTracingMiddleware(tracer opentracing.Tracer) *TracingMiddleware {
return &TracingMiddleware{
tracer: tracer,
}
}

// Middleware applies the tracing middleware to the handler
func (m *TracingMiddleware) Middleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
spanContext, _ := m.tracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(r.Header))
span := m.tracer.StartSpan("query", ext.RPCServerOption(spanContext))
c := opentracing.ContextWithSpan(r.Context(), span)
h.ServeHTTP(rw, r.WithContext(c))
span.Finish()
})
}
```

</details>

3. Add the tracer to the resolver

- With graph-gophers

```go
parsedSchema := graphql.MustParseSchema(schema, resolver, graphql.Tracer(trace.OpenTracingTracer{}))
```

- With gqlgen

```go
gqlserver.Use(support.NewGqlgenOpenTracing(tracer))
```
8 changes: 0 additions & 8 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,3 @@ Exposes the GraphQL playground on `/playground`.
```

You access the GraphQL playground by visiting `http://localhost:<gateway-port>/playground` in your browser.

## Open Tracing (Jaeger)

The Jaeger plugin captures and sends traces to a Jaeger server.

Configuration is done through environment variables, see the [Jaeger
documentation](https://github.com/jaegertracing/jaeger-client-go#environment-variables)
for more information.
Loading

0 comments on commit 7927df3

Please sign in to comment.