-
Notifications
You must be signed in to change notification settings - Fork 116
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(router): support traffic shaping rules on subgraph level (#1438)
- Loading branch information
Showing
13 changed files
with
913 additions
and
31 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
package integration_test | ||
|
||
import ( | ||
"github.com/stretchr/testify/require" | ||
"github.com/wundergraph/cosmo/router-tests/testenv" | ||
"github.com/wundergraph/cosmo/router/core" | ||
"github.com/wundergraph/cosmo/router/pkg/config" | ||
"net/http" | ||
"testing" | ||
"time" | ||
) | ||
|
||
func TestTimeouts(t *testing.T) { | ||
t.Parallel() | ||
|
||
const queryEmployeeWithHobby = `{ | ||
employee(id: 1) { | ||
id | ||
hobbies { | ||
... on Gaming { | ||
name | ||
} | ||
} | ||
} | ||
}` | ||
|
||
const queryEmployeeWithNoHobby = `{ | ||
employee(id: 1) { | ||
id | ||
} | ||
}` | ||
|
||
t.Run("applies RequestTimeout", func(t *testing.T) { | ||
t.Parallel() | ||
|
||
hobbySubgraphSleep := testenv.SubgraphsConfig{ | ||
Hobbies: testenv.SubgraphConfig{ | ||
Middleware: func(handler http.Handler) http.Handler { | ||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
w.WriteHeader(http.StatusOK) | ||
time.Sleep(5 * time.Millisecond) // Slow response | ||
w.Write([]byte("Hello, world!")) | ||
}) | ||
}, | ||
}, | ||
} | ||
|
||
trafficConfig := config.TrafficShapingRules{ | ||
All: config.GlobalSubgraphRequestRule{ | ||
RequestTimeout: 10 * time.Millisecond, | ||
}, | ||
Subgraphs: map[string]*config.GlobalSubgraphRequestRule{ | ||
"hobbies": { | ||
RequestTimeout: 3 * time.Millisecond, | ||
}, | ||
}, | ||
} | ||
t.Run("applied subgraph timeout to request", func(t *testing.T) { | ||
t.Parallel() | ||
|
||
testenv.Run(t, &testenv.Config{ | ||
Subgraphs: hobbySubgraphSleep, | ||
RouterOptions: []core.Option{ | ||
core.WithSubgraphTransportOptions( | ||
core.NewSubgraphTransportOptions(trafficConfig)), | ||
}, | ||
}, func(t *testing.T, xEnv *testenv.Environment) { | ||
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ | ||
Query: queryEmployeeWithHobby, | ||
}) | ||
require.Equal(t, `{"errors":[{"message":"Failed to fetch from Subgraph 'hobbies' at Path 'employee'."}],"data":{"employee":{"id":1,"hobbies":null}}}`, res.Body) | ||
}) | ||
}) | ||
|
||
t.Run("Subgraph timeout options don't affect unrelated subgraph", func(t *testing.T) { | ||
t.Parallel() | ||
|
||
testenv.Run(t, &testenv.Config{ | ||
Subgraphs: hobbySubgraphSleep, | ||
RouterOptions: []core.Option{ | ||
core.WithSubgraphTransportOptions( | ||
core.NewSubgraphTransportOptions(trafficConfig)), | ||
}, | ||
}, func(t *testing.T, xEnv *testenv.Environment) { | ||
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ | ||
Query: queryEmployeeWithNoHobby, | ||
}) | ||
require.Equal(t, `{"data":{"employee":{"id":1}}}`, res.Body) | ||
}) | ||
}) | ||
}) | ||
|
||
t.Run("ResponseHeaderTimeout exceeded", func(t *testing.T) { | ||
t.Parallel() | ||
|
||
hobbySubgraphSleep := testenv.SubgraphsConfig{ | ||
Hobbies: testenv.SubgraphConfig{ | ||
Middleware: func(handler http.Handler) http.Handler { | ||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
w.WriteHeader(http.StatusOK) | ||
time.Sleep(5 * time.Millisecond) // Slow response | ||
w.Write([]byte("Hello, world!")) | ||
}) | ||
}, | ||
}, | ||
} | ||
|
||
trafficConfig := config.TrafficShapingRules{ | ||
All: config.GlobalSubgraphRequestRule{ | ||
RequestTimeout: 10 * time.Millisecond, | ||
}, | ||
Subgraphs: map[string]*config.GlobalSubgraphRequestRule{ | ||
"hobbies": { | ||
ResponseHeaderTimeout: 3 * time.Millisecond, | ||
}, | ||
}, | ||
} | ||
|
||
testenv.Run(t, &testenv.Config{ | ||
Subgraphs: hobbySubgraphSleep, | ||
RouterOptions: []core.Option{ | ||
core.WithSubgraphTransportOptions( | ||
core.NewSubgraphTransportOptions(trafficConfig)), | ||
}, | ||
}, func(t *testing.T, xEnv *testenv.Environment) { | ||
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ | ||
Query: queryEmployeeWithNoHobby, | ||
}) | ||
require.Equal(t, `{"data":{"employee":{"id":1}}}`, res.Body) | ||
}) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package core | ||
|
||
import ( | ||
"context" | ||
"go.uber.org/zap" | ||
"net/http" | ||
) | ||
|
||
type TimeoutTransport struct { | ||
defaultTransport http.RoundTripper | ||
logger *zap.Logger | ||
subgraphTrippers map[string]*http.Transport | ||
opts *SubgraphTransportOptions | ||
} | ||
|
||
func NewTimeoutTransport(transportOpts *SubgraphTransportOptions, roundTripper http.RoundTripper, logger *zap.Logger, proxy ProxyFunc) *TimeoutTransport { | ||
tt := &TimeoutTransport{ | ||
defaultTransport: roundTripper, | ||
logger: logger, | ||
subgraphTrippers: map[string]*http.Transport{}, | ||
opts: transportOpts, | ||
} | ||
|
||
for subgraph, subgraphOpts := range transportOpts.SubgraphMap { | ||
if subgraphOpts != nil { | ||
tt.subgraphTrippers[subgraph] = newHTTPTransport(*subgraphOpts, proxy) | ||
} | ||
} | ||
|
||
return tt | ||
} | ||
|
||
func (tt *TimeoutTransport) RoundTrip(req *http.Request) (*http.Response, error) { | ||
if req == nil { | ||
return nil, nil | ||
} | ||
|
||
rq := getRequestContext(req.Context()) | ||
if rq == nil { | ||
return nil, nil | ||
} | ||
subgraph := rq.ActiveSubgraph(req) | ||
if subgraph != nil && subgraph.Name != "" && tt.subgraphTrippers[subgraph.Name] != nil { | ||
timeout := tt.opts.SubgraphMap[subgraph.Name].RequestTimeout | ||
if timeout > 0 { | ||
ctx, cancel := context.WithTimeout(req.Context(), timeout) | ||
defer cancel() | ||
return tt.subgraphTrippers[subgraph.Name].RoundTrip(req.WithContext(ctx)) | ||
} | ||
return tt.subgraphTrippers[subgraph.Name].RoundTrip(req) | ||
} | ||
return tt.defaultTransport.RoundTrip(req) | ||
} |
Oops, something went wrong.