diff --git a/execution/engine/engine_config.go b/execution/engine/engine_config.go index 209e83fe4c..4230273164 100644 --- a/execution/engine/engine_config.go +++ b/execution/engine/engine_config.go @@ -22,6 +22,7 @@ type Configuration struct { schema *graphql.Schema plannerConfig plan.Configuration websocketBeforeStartHook WebsocketBeforeStartHook + enableScheduleFetches bool } func NewConfiguration(schema *graphql.Schema) Configuration { @@ -79,6 +80,11 @@ func (e *Configuration) EnableMultiFetch() { e.plannerConfig.EnableMultiFetch = true } +// EnableScheduleFetches organizes fetches into component-split, chain-inlined trees. +func (e *Configuration) EnableScheduleFetches() { + e.enableScheduleFetches = true +} + type dataSourceGeneratorOptions struct { streamingClient *http.Client subscriptionType SubscriptionType diff --git a/execution/engine/execution_engine.go b/execution/engine/execution_engine.go index 8029cbf3bc..25f3f93556 100644 --- a/execution/engine/execution_engine.go +++ b/execution/engine/execution_engine.go @@ -158,6 +158,9 @@ func NewExecutionEngine(ctx context.Context, logger abstractlogger.Logger, engin if engineConfig.plannerConfig.EnableMultiFetch { postProcessorOptions = append(postProcessorOptions, postprocess.EnableMultiFetch()) } + if engineConfig.enableScheduleFetches { + postProcessorOptions = append(postProcessorOptions, postprocess.EnableScheduleFetches()) + } return &ExecutionEngine{ logger: logger, diff --git a/execution/engine/execution_engine_schedule_fetches_test.go b/execution/engine/execution_engine_schedule_fetches_test.go new file mode 100644 index 0000000000..9ef12acb98 --- /dev/null +++ b/execution/engine/execution_engine_schedule_fetches_test.go @@ -0,0 +1,260 @@ +package engine + +import ( + "context" + "testing" + + "github.com/jensneuse/abstractlogger" + "github.com/stretchr/testify/require" + + "github.com/wundergraph/graphql-go-tools/execution/graphql" + "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/datasource/graphql_datasource" + "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/plan" + "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" +) + +// Two independent fetch chains: +// - alpha root -> b entity fetch, +// - and b root -> alpha entity fetch. +const scheduleFetchesSchema = ` + type Query { + a: A + aTwo: A + b: B + } + type A { + id: ID! + bField: String + } + type B { + id: ID! + aField: String + } +` +const scheduleFetchesAlphaSDL = ` + type Query { + a: A + aTwo: A + } + type A @key(fields: "id") { + id: ID! + } + type B @key(fields: "id") { + id: ID! + aField: String + } +` +const scheduleFetchesBetaSDL = ` + type Query { + b: B + } + type B @key(fields: "id") { + id: ID! + } + type A @key(fields: "id") { + id: ID! + bField: String + } +` +const scheduleFetchesQuery = ` + query { + a { bField } + b { aField } + } +` +const scheduleFetchesCombinedQuery = ` + query { + a { bField } + aTwo { bField } + b { aField } + } +` + +const ( + scheduleFetchesAlphaRootBody = `{"query":"{a {__typename id}}"}` + scheduleFetchesAlphaRootData = `{"data":{"a":{"__typename":"A","id":"1"}}}` + scheduleFetchesAlphaEntityBody = `{"query":"query($representations: [_Any!]!){_entities(representations: $representations){... on B {__typename aField}}}","variables":{"representations":[{"__typename":"B","id":"2"}]}}` + scheduleFetchesAlphaEntityData = `{"data":{"_entities":[{"__typename":"B","aField":"a"}]}}` + + scheduleFetchesBetaRootBody = `{"query":"{b {__typename id}}"}` + scheduleFetchesBetaRootData = `{"data":{"b":{"__typename":"B","id":"2"}}}` + scheduleFetchesBetaEntityBody = `{"query":"query($representations: [_Any!]!){_entities(representations: $representations){... on A {__typename bField}}}","variables":{"representations":[{"__typename":"A","id":"1"}]}}` + scheduleFetchesBetaEntityData = `{"data":{"_entities":[{"__typename":"A","bField":"b"}]}}` + + scheduleFetchesClientResponse = `{"data":{"a":{"bField":"b"},"b":{"aField":"a"}}}` + + scheduleFetchesCombinedAlphaRootBody = `{"query":"{a {__typename id} aTwo {__typename id}}"}` + scheduleFetchesCombinedAlphaRootData = `{"data":{"a":{"__typename":"A","id":"1"},"aTwo":{"__typename":"A","id":"3"}}}` + scheduleFetchesMergedBetaBody = `{"query":"query($representations_f1: [_Any!]!, $includeF1: Boolean!, $representations_f2: [_Any!]!, $includeF2: Boolean!){f1: _entities(representations: $representations_f1)@include(if: $includeF1) {... on A {__typename bField}} f2: _entities(representations: $representations_f2)@include(if: $includeF2) {... on A {__typename bField}}}","variables":{"representations_f1":[{"__typename":"A","id":"1"}],"includeF1":true,"representations_f2":[{"__typename":"A","id":"3"}],"includeF2":true}}` + scheduleFetchesMergedBetaData = `{"data":{"f1":[{"__typename":"A","bField":"b"}],"f2":[{"__typename":"A","bField":"b2"}]}}` + + scheduleFetchesCombinedClientResponse = `{"data":{"a":{"bField":"b"},"aTwo":{"bField":"b2"},"b":{"aField":"a"}}}` +) + +func scheduleFetchesDataSource(t *testing.T, name, sdl string, rec *multiFetchRecorder, responses map[string]sendResponse, rootNodes []plan.TypeField) plan.DataSource { + t.Helper() + + return mustGraphqlDataSourceConfiguration(t, + name, + mustFactory(t, recordingClient(t, rec, name, "/", responses)), + &plan.DataSourceMetadata{ + RootNodes: rootNodes, + FederationMetaData: plan.FederationMetaData{ + Keys: plan.FederationFieldConfigurations{ + {TypeName: "A", SelectionSet: "id"}, + {TypeName: "B", SelectionSet: "id"}, + }, + }, + }, + mustConfiguration(t, graphql_datasource.ConfigurationInput{ + Fetch: &graphql_datasource.FetchConfiguration{URL: "http://" + name + "/", Method: "POST"}, + SchemaConfiguration: mustSchemaConfig(t, + &graphql_datasource.FederationConfiguration{Enabled: true, ServiceSDL: sdl}, + sdl, + ), + }), + ) +} + +func scheduleFetchesDataSources(t *testing.T, aRec, bRec *multiFetchRecorder) []plan.DataSource { + t.Helper() + + a := scheduleFetchesDataSource(t, "a", scheduleFetchesAlphaSDL, aRec, + map[string]sendResponse{ + scheduleFetchesAlphaRootBody: {statusCode: 200, body: scheduleFetchesAlphaRootData}, + scheduleFetchesAlphaEntityBody: {statusCode: 200, body: scheduleFetchesAlphaEntityData}, + scheduleFetchesCombinedAlphaRootBody: {statusCode: 200, body: scheduleFetchesCombinedAlphaRootData}, + }, + []plan.TypeField{ + {TypeName: "Query", FieldNames: []string{"a", "aTwo"}}, + {TypeName: "A", FieldNames: []string{"id"}}, + {TypeName: "B", FieldNames: []string{"id", "aField"}}, + }) + + b := scheduleFetchesDataSource(t, "b", scheduleFetchesBetaSDL, bRec, + map[string]sendResponse{ + scheduleFetchesBetaRootBody: {statusCode: 200, body: scheduleFetchesBetaRootData}, + scheduleFetchesBetaEntityBody: {statusCode: 200, body: scheduleFetchesBetaEntityData}, + scheduleFetchesMergedBetaBody: {statusCode: 200, body: scheduleFetchesMergedBetaData}, + }, + []plan.TypeField{ + {TypeName: "Query", FieldNames: []string{"b"}}, + {TypeName: "B", FieldNames: []string{"id"}}, + {TypeName: "A", FieldNames: []string{"id", "bField"}}, + }) + + return []plan.DataSource{a, b} +} + +// runScheduleFetchesQuery plans and executes the query and returns +// the response body, the organized fetch tree, and the request bodies each subgraph received. +func runScheduleFetchesQuery(t *testing.T, query string, enableMultiFetch, enableScheduleFetches bool) (string, *resolve.FetchTreeNode, []string, []string) { + t.Helper() + + schema, err := graphql.NewSchemaFromString(scheduleFetchesSchema) + require.NoError(t, err) + + aRec, bRec := &multiFetchRecorder{}, &multiFetchRecorder{} + engineConf := NewConfiguration(schema) + engineConf.SetDataSources(scheduleFetchesDataSources(t, aRec, bRec)) + if enableMultiFetch { + engineConf.EnableMultiFetch() + } + if enableScheduleFetches { + engineConf.EnableScheduleFetches() + } + + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + + engine, err := NewExecutionEngine(ctx, abstractlogger.Noop{}, engineConf, resolve.ResolverOptions{MaxConcurrency: 1024}) + require.NoError(t, err) + + operation := graphql.Request{Query: query} + resultWriter := graphql.NewEngineResultWriter() + require.NoError(t, engine.Execute(ctx, &operation, &resultWriter)) + + require.Equal(t, 1, engine.executionPlanCache.Len()) + _, cachedPlan, ok := engine.executionPlanCache.GetOldest() + require.True(t, ok) + syncPlan, ok := cachedPlan.(*plan.SynchronousResponsePlan) + require.True(t, ok) + return resultWriter.String(), syncPlan.Response.Fetches, aRec.requests(), bRec.requests() +} + +func nodeKinds(nodes []*resolve.FetchTreeNode) []resolve.FetchTreeNodeKind { + kinds := make([]resolve.FetchTreeNodeKind, len(nodes)) + for i, n := range nodes { + kinds[i] = n.Kind + } + return kinds +} + +func TestExecutionEngine_ScheduleFetches(t *testing.T) { + // Request bodies are asserted as sets: under the scheduler the chains progress independently, + // so arrival order at a host is not deterministic. + cases := []struct { + name string + query string + multiFetch, schedule bool + response string + aReqs, bReqs []string + rootKind resolve.FetchTreeNodeKind + childKinds []resolve.FetchTreeNodeKind + }{ + { + name: "default organizes legacy waves", + query: scheduleFetchesQuery, + response: scheduleFetchesClientResponse, + aReqs: []string{scheduleFetchesAlphaRootBody, scheduleFetchesAlphaEntityBody}, + bReqs: []string{scheduleFetchesBetaRootBody, scheduleFetchesBetaEntityBody}, + rootKind: resolve.FetchTreeNodeKindSequence, + childKinds: []resolve.FetchTreeNodeKind{resolve.FetchTreeNodeKindParallel, resolve.FetchTreeNodeKindParallel}, + }, + { + name: "scheduling organizes independent inlined chains", + query: scheduleFetchesQuery, + schedule: true, + response: scheduleFetchesClientResponse, + aReqs: []string{scheduleFetchesAlphaRootBody, scheduleFetchesAlphaEntityBody}, + bReqs: []string{scheduleFetchesBetaRootBody, scheduleFetchesBetaEntityBody}, + rootKind: resolve.FetchTreeNodeKindParallel, + childKinds: []resolve.FetchTreeNodeKind{resolve.FetchTreeNodeKindSequence, resolve.FetchTreeNodeKindSequence}, + }, + { + name: "multi fetch without scheduling merges within legacy waves", + query: scheduleFetchesCombinedQuery, + multiFetch: true, + response: scheduleFetchesCombinedClientResponse, + aReqs: []string{scheduleFetchesCombinedAlphaRootBody, scheduleFetchesAlphaEntityBody}, + // The two same-wave b entity fetches merged into one aliased request, + // while the tree keeps the legacy wave shape. + bReqs: []string{scheduleFetchesBetaRootBody, scheduleFetchesMergedBetaBody}, + rootKind: resolve.FetchTreeNodeKindSequence, + childKinds: []resolve.FetchTreeNodeKind{resolve.FetchTreeNodeKindParallel, resolve.FetchTreeNodeKindParallel}, + }, + { + name: "multi fetch and scheduling together produce two inlined chains", + query: scheduleFetchesCombinedQuery, + multiFetch: true, + schedule: true, + response: scheduleFetchesCombinedClientResponse, + aReqs: []string{scheduleFetchesCombinedAlphaRootBody, scheduleFetchesAlphaEntityBody}, + // The two same-wave b entity fetches merged into one aliased request. + bReqs: []string{scheduleFetchesBetaRootBody, scheduleFetchesMergedBetaBody}, + rootKind: resolve.FetchTreeNodeKindParallel, + childKinds: []resolve.FetchTreeNodeKind{resolve.FetchTreeNodeKindSequence, resolve.FetchTreeNodeKindSequence}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + response, fetches, aReqs, bReqs := runScheduleFetchesQuery(t, tc.query, tc.multiFetch, tc.schedule) + require.Equal(t, tc.response, response) + require.ElementsMatch(t, tc.aReqs, aReqs) + require.ElementsMatch(t, tc.bReqs, bReqs) + require.Equal(t, tc.rootKind, fetches.Kind) + require.Equal(t, tc.childKinds, nodeKinds(fetches.ChildNodes)) + }) + } +} diff --git a/execution/go.mod b/execution/go.mod index 5d4926d1fb..d21c875ed9 100644 --- a/execution/go.mod +++ b/execution/go.mod @@ -67,13 +67,13 @@ require ( github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/mod v0.33.0 // indirect - golang.org/x/net v0.52.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.42.0 // indirect - golang.org/x/term v0.41.0 // indirect - golang.org/x/text v0.35.0 // indirect - golang.org/x/tools v0.42.0 // indirect + golang.org/x/mod v0.37.0 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/term v0.44.0 // indirect + golang.org/x/text v0.39.0 // indirect + golang.org/x/tools v0.47.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/execution/go.sum b/execution/go.sum index c257716c37..a936169063 100644 --- a/execution/go.sum +++ b/execution/go.sum @@ -191,19 +191,19 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -217,15 +217,15 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= @@ -233,8 +233,8 @@ golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/go.work.sum b/go.work.sum index 20390f9ce0..61178be133 100644 --- a/go.work.sum +++ b/go.work.sum @@ -406,8 +406,6 @@ github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0 github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= @@ -479,8 +477,6 @@ github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+x github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083/go.mod h1:eOTL6acwctsN4F3b7YE+eE2t8zcJ/doLm9sZzsxxxrE= github.com/wundergraph/go-arena v0.0.0-20251008210416-55cb97e6f68f h1:5snewyMaIpajTu4wj22L/DgrGimICqXtUVjkZInBH3Y= github.com/wundergraph/go-arena v0.0.0-20251008210416-55cb97e6f68f/go.mod h1:ROOysEHWJjLQ8FSfNxZCziagb7Qw2nXY3/vgKRh7eWw= -github.com/wundergraph/go-arena v1.3.0 h1:n0ng5a1vbd8YGq1u3rMr0vPU5f6AZ1BXIiUhL1UIok8= -github.com/wundergraph/go-arena v1.3.0/go.mod h1:ROOysEHWJjLQ8FSfNxZCziagb7Qw2nXY3/vgKRh7eWw= github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.231/go.mod h1:ErOQH1ki2+SZB8JjpTyGVnoBpg5picIyjvuWQJP4abg= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= @@ -573,6 +569,8 @@ golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= @@ -653,6 +651,8 @@ golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc h1:bH6xUXay0AIFMElXG2r golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 h1:bTLqdHv7xrGlFbvf5/TXNxy/iUwwdkjhqQTJDjW7aj0= golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4/go.mod h1:g5NllXBEermZrmR51cJDQxmJUHUOfRAaNyWBM+R+548= +golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 h1:nwGZBCt+FnXUrGsj5vjzAsEmkcaFvd82BbOjECiFYZc= +golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57/go.mod h1:3AWMyWHS+caVoiEXpiq6+tzKA40J4vQT3MYr80ZtQpc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= @@ -668,6 +668,7 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= @@ -679,6 +680,7 @@ golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= diff --git a/v2/go.mod b/v2/go.mod index 9d3eefbf40..8e1e278d9b 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -32,10 +32,9 @@ require ( github.com/wundergraph/astjson v1.1.0 github.com/wundergraph/go-arena v1.3.0 go.uber.org/goleak v1.3.0 - golang.org/x/net v0.52.0 - golang.org/x/sync v0.20.0 - golang.org/x/sys v0.42.0 - golang.org/x/text v0.35.0 + golang.org/x/sync v0.21.0 + golang.org/x/sys v0.46.0 + golang.org/x/text v0.39.0 gonum.org/v1/gonum v0.17.0 google.golang.org/grpc v1.80.0 google.golang.org/protobuf v1.36.11 @@ -75,9 +74,10 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/mod v0.33.0 // indirect - golang.org/x/term v0.41.0 // indirect - golang.org/x/tools v0.42.0 // indirect + golang.org/x/mod v0.37.0 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/term v0.44.0 // indirect + golang.org/x/tools v0.47.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect diff --git a/v2/go.sum b/v2/go.sum index 30bc207aeb..c9f08c8a8d 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -183,19 +183,19 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -209,15 +209,15 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= @@ -225,8 +225,8 @@ golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_defer_test.go b/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_defer_test.go index 5d0c2e084c..a6aeb9adb9 100644 --- a/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_defer_test.go +++ b/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_defer_test.go @@ -168,7 +168,7 @@ func TestGraphQLDataSourceDefer(t *testing.T) { }, }, planConfiguration, - WithDefaultCustomPostProcessor(postprocess.DisableResolveInputTemplates(), postprocess.DisableCreateConcreteSingleFetchTypes(), postprocess.DisableCreateParallelNodes(), postprocess.DisableMergeFields(), postprocess.DisableExtractDeferFetches()), + WithPostProcessor(postprocess.DisableResolveInputTemplates(), postprocess.DisableCreateConcreteSingleFetchTypes(), postprocess.DisableCreateParallelNodes(), postprocess.DisableMergeFields(), postprocess.DisableExtractDeferFetches()), WithDefer(), WithCalculateFieldDependencies(), ) @@ -746,7 +746,7 @@ func TestGraphQLDataSourceDefer(t *testing.T) { }, }, planConfiguration, - WithDefaultCustomPostProcessor(postprocess.DisableResolveInputTemplates(), postprocess.DisableCreateConcreteSingleFetchTypes(), postprocess.DisableCreateParallelNodes(), postprocess.DisableMergeFields(), postprocess.DisableExtractDeferFetches()), + WithPostProcessor(postprocess.DisableResolveInputTemplates(), postprocess.DisableCreateConcreteSingleFetchTypes(), postprocess.DisableCreateParallelNodes(), postprocess.DisableMergeFields(), postprocess.DisableExtractDeferFetches()), WithDefer(), WithCalculateFieldDependencies(), ) diff --git a/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_entity_interfaces_test.go b/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_entity_interfaces_test.go index ad3df31e65..ec566660e1 100644 --- a/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_entity_interfaces_test.go +++ b/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_entity_interfaces_test.go @@ -4254,7 +4254,7 @@ func TestGraphQLDataSourceFederationEntityInterfaces(t *testing.T) { }, }, planConfiguration, - WithDefaultCustomPostProcessor(postprocess.DisableResolveInputTemplates()), + WithPostProcessor(postprocess.DisableResolveInputTemplates()), )) }) @@ -4554,7 +4554,7 @@ func TestGraphQLDataSourceFederationEntityInterfaces(t *testing.T) { }, }, planConfiguration, - WithDefaultCustomPostProcessor(postprocess.DisableResolveInputTemplates()), + WithPostProcessor(postprocess.DisableResolveInputTemplates()), )) }) @@ -4924,7 +4924,7 @@ func TestGraphQLDataSourceFederationEntityInterfaces(t *testing.T) { }, }, planConfiguration, - WithDefaultCustomPostProcessor(postprocess.DisableResolveInputTemplates()), + WithPostProcessor(postprocess.DisableResolveInputTemplates()), // WithPrintPlan(), )) }) @@ -5126,7 +5126,7 @@ func TestGraphQLDataSourceFederationEntityInterfaces(t *testing.T) { }, }, planConfiguration, - WithDefaultCustomPostProcessor(postprocess.DisableResolveInputTemplates()), + WithPostProcessor(postprocess.DisableResolveInputTemplates()), // WithPrintPlan(), )) }) @@ -5199,7 +5199,7 @@ func TestGraphQLDataSourceFederationEntityInterfaces(t *testing.T) { }, }, planConfiguration, - WithDefaultCustomPostProcessor(postprocess.DisableResolveInputTemplates()), + WithPostProcessor(postprocess.DisableResolveInputTemplates()), // WithPrintPlan(), )) }) @@ -5307,7 +5307,7 @@ func TestGraphQLDataSourceFederationEntityInterfaces(t *testing.T) { }, }, planConfiguration, - WithDefaultCustomPostProcessor(postprocess.DisableResolveInputTemplates()), + WithPostProcessor(postprocess.DisableResolveInputTemplates()), // WithPrintPlan(), )) }) diff --git a/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go b/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go index 1651f45feb..a302a402a0 100644 --- a/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go +++ b/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go @@ -1775,7 +1775,7 @@ func TestGraphQLDataSourceFederation(t *testing.T) { planConfiguration, WithFieldInfo(), // default post-processor options, but with authorization coordinate collection enabled - WithDefaultCustomPostProcessor( + WithPostProcessor( postprocess.DisableResolveInputTemplates(), postprocess.DisableCreateConcreteSingleFetchTypes(), postprocess.DisableCreateParallelNodes(), @@ -16439,7 +16439,7 @@ func TestGraphQLDataSourceFederation(t *testing.T) { "Query", expectedPlans, planConfiguration, - WithDefaultCustomPostProcessor(postprocess.DisableResolveInputTemplates(), postprocess.DisableCreateConcreteSingleFetchTypes(), postprocess.DisableOrderSequenceByDependencies(), postprocess.DisableMergeFields()), + WithPostProcessor(postprocess.DisableResolveInputTemplates(), postprocess.DisableCreateConcreteSingleFetchTypes(), postprocess.DisableOrderSequenceByDependencies(), postprocess.DisableMergeFields()), ) }) @@ -16573,7 +16573,7 @@ func TestGraphQLDataSourceFederation(t *testing.T) { "Query", expectedPlan, planConfiguration, - WithDefaultCustomPostProcessor(postprocess.DisableResolveInputTemplates(), postprocess.DisableCreateConcreteSingleFetchTypes(), postprocess.DisableOrderSequenceByDependencies(), postprocess.DisableMergeFields()), + WithPostProcessor(postprocess.DisableResolveInputTemplates(), postprocess.DisableCreateConcreteSingleFetchTypes(), postprocess.DisableOrderSequenceByDependencies(), postprocess.DisableMergeFields()), ) }) @@ -16706,7 +16706,7 @@ func TestGraphQLDataSourceFederation(t *testing.T) { "Query", expectedPlan, planConfiguration, - WithDefaultCustomPostProcessor(postprocess.DisableResolveInputTemplates(), postprocess.DisableCreateConcreteSingleFetchTypes(), postprocess.DisableOrderSequenceByDependencies(), postprocess.DisableMergeFields()), + WithPostProcessor(postprocess.DisableResolveInputTemplates(), postprocess.DisableCreateConcreteSingleFetchTypes(), postprocess.DisableOrderSequenceByDependencies(), postprocess.DisableMergeFields()), ) }) }) diff --git a/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_multi_fetch_test.go b/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_multi_fetch_test.go index 5647f3f710..c1a66205df 100644 --- a/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_multi_fetch_test.go +++ b/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_multi_fetch_test.go @@ -353,7 +353,7 @@ func TestGraphQLDataSourceFederation_MultiFetch(t *testing.T) { }, multiFetchPlanConfig(t, true), WithFieldInfo(), - WithDefaultCustomPostProcessor(postprocess.EnableMultiFetch()), + WithPostProcessor(postprocess.EnableMultiFetch()), )) t.Run("flag off keeps two separate entity fetches", RunTest( @@ -490,7 +490,9 @@ func TestGraphQLDataSourceFederation_MultiFetch(t *testing.T) { }, multiFetchPlanConfig(t, false), WithFieldInfo(), - WithDefaultCustomPostProcessor(), + // Full default pipeline with only the scheduler replaced by the legacy + // organizers — the fixture pins rendered inputs and legacy wave shapes. + WithPostProcessor(), )) } @@ -817,7 +819,7 @@ func TestGraphQLDataSourceFederation_MultiFetch_ThreeFetchGroup(t *testing.T) { }, config, WithFieldInfo(), - WithDefaultCustomPostProcessor(postprocess.EnableMultiFetch()), + WithPostProcessor(postprocess.EnableMultiFetch()), )) } @@ -1102,7 +1104,7 @@ func TestGraphQLDataSourceFederation_MultiFetch_AdditionalVariables(t *testing.T }, multiFetchArgPlanConfig(t), WithFieldInfo(), - WithDefaultCustomPostProcessor(postprocess.EnableMultiFetch()), + WithPostProcessor(postprocess.EnableMultiFetch()), )) t.Run("same client variable through both members", RunTest( @@ -1162,7 +1164,7 @@ func TestGraphQLDataSourceFederation_MultiFetch_AdditionalVariables(t *testing.T }, multiFetchArgPlanConfig(t), WithFieldInfo(), - WithDefaultCustomPostProcessor(postprocess.EnableMultiFetch()), + WithPostProcessor(postprocess.EnableMultiFetch()), )) // A default-valued client variable ($first: Int = 10) merges without error. @@ -1230,7 +1232,7 @@ func TestGraphQLDataSourceFederation_MultiFetch_AdditionalVariables(t *testing.T }, multiFetchArgPlanConfig(t), WithFieldInfo(), - WithDefaultCustomPostProcessor(postprocess.EnableMultiFetch()), + WithPostProcessor(postprocess.EnableMultiFetch()), )) } @@ -1571,7 +1573,7 @@ func TestGraphQLDataSourceFederation_MultiFetch_WaveSeparation(t *testing.T) { }, config, WithFieldInfo(), - WithDefaultCustomPostProcessor(postprocess.EnableMultiFetch()), + WithPostProcessor(postprocess.EnableMultiFetch()), )) } @@ -1996,7 +1998,7 @@ func TestGraphQLDataSourceFederation_MultiFetch_Subscription(t *testing.T) { }, config, WithFieldInfo(), - WithDefaultCustomPostProcessor(postprocess.EnableMultiFetch()), + WithPostProcessor(postprocess.EnableMultiFetch()), )) } @@ -2100,7 +2102,7 @@ func TestGraphQLDataSourceFederation_MultiFetch_RepresentationsCollision(t *test }, multiFetchArgPlanConfig(t), WithFieldInfo(), - WithDefaultCustomPostProcessor(postprocess.EnableMultiFetch()), + WithPostProcessor(postprocess.EnableMultiFetch()), )(t) }) } diff --git a/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_connect_test.go b/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_connect_test.go index 21d676e0a4..a1e6274da4 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_connect_test.go +++ b/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_connect_test.go @@ -10,8 +10,6 @@ import ( "github.com/stretchr/testify/require" "github.com/tidwall/gjson" - "golang.org/x/net/http2" - "golang.org/x/net/http2/h2c" "google.golang.org/grpc/metadata" "github.com/wundergraph/graphql-go-tools/v2/pkg/astparser" @@ -36,8 +34,10 @@ func setupTestConnectServer(t testing.TB) (baseURL string, cleanup func()) { mux := http.NewServeMux() mux.Handle(productv1connect.NewProductServiceHandler(connectImpl)) - srv := httptest.NewUnstartedServer(h2c.NewHandler(mux, &http2.Server{})) - srv.EnableHTTP2 = true + srv := httptest.NewUnstartedServer(mux) + srv.Config.Protocols = new(http.Protocols) + srv.Config.Protocols.SetHTTP1(true) + srv.Config.Protocols.SetUnencryptedHTTP2(true) srv.Start() cleanup = srv.Close diff --git a/v2/pkg/engine/datasourcetesting/datasourcetesting.go b/v2/pkg/engine/datasourcetesting/datasourcetesting.go index 08147d7d02..67dbd2e07f 100644 --- a/v2/pkg/engine/datasourcetesting/datasourcetesting.go +++ b/v2/pkg/engine/datasourcetesting/datasourcetesting.go @@ -61,8 +61,7 @@ func WithDefaultPostProcessor() func(*testOptions) { } } -func WithDefaultCustomPostProcessor(options ...postprocess.ProcessorOption) func(*testOptions) { - // TODO: rename to WithPostProcessor +func WithPostProcessor(options ...postprocess.ProcessorOption) func(*testOptions) { return func(o *testOptions) { o.postProcessor = postprocess.NewProcessor(options...) } diff --git a/v2/pkg/engine/postprocess/add_missing_nested_dependencies_test.go b/v2/pkg/engine/postprocess/add_missing_nested_dependencies_test.go index e24eaef73c..4324492065 100644 --- a/v2/pkg/engine/postprocess/add_missing_nested_dependencies_test.go +++ b/v2/pkg/engine/postprocess/add_missing_nested_dependencies_test.go @@ -4,138 +4,28 @@ import ( "testing" "github.com/stretchr/testify/require" - - "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" ) func TestAddMissingNestedDependencies_ProcessFetchTree(t *testing.T) { t.Run("add missing dependencies to nested fetches on same merge path", func(t *testing.T) { - input := resolve.Sequence( - resolve.Single(&resolve.SingleFetch{ - FetchConfiguration: resolve.FetchConfiguration{ - Input: "a", - PostProcessing: resolve.PostProcessingConfiguration{ - MergePath: []string{"a"}, - }, - }, - FetchDependencies: resolve.FetchDependencies{ - FetchID: 0, - }, - }), - resolve.Single(&resolve.SingleFetch{ - FetchConfiguration: resolve.FetchConfiguration{ - Input: "b", - PostProcessing: resolve.PostProcessingConfiguration{ - MergePath: []string{"b"}, - }, - }, - FetchDependencies: resolve.FetchDependencies{ - FetchID: 1, - }, - }), - resolve.SingleWithPath(&resolve.SingleFetch{ - FetchConfiguration: resolve.FetchConfiguration{ - Input: "c", - }, - FetchDependencies: resolve.FetchDependencies{ - FetchID: 2, - }, - }, "a", resolve.ObjectPath("a")), - resolve.SingleWithPath(&resolve.SingleFetch{ - FetchConfiguration: resolve.FetchConfiguration{ - Input: "d", - }, - FetchDependencies: resolve.FetchDependencies{ - FetchID: 3, - }, - }, "b.c", resolve.ObjectPath("b"), resolve.ObjectPath("c")), - resolve.SingleWithPath(&resolve.SingleFetch{ - FetchConfiguration: resolve.FetchConfiguration{ - Input: "x", - }, - FetchDependencies: resolve.FetchDependencies{ - FetchID: 4, - DependsOnFetchIDs: []int{0}, - }, - }, "a", resolve.ObjectPath("a")), - resolve.Single(&resolve.SingleFetch{ - FetchConfiguration: resolve.FetchConfiguration{ - Input: "y", - PostProcessing: resolve.PostProcessingConfiguration{ - MergePath: []string{"y"}, - }, - }, - FetchDependencies: resolve.FetchDependencies{ - FetchID: 5, - }, - }), - ) - - expected := resolve.Sequence( - resolve.Single(&resolve.SingleFetch{ - FetchConfiguration: resolve.FetchConfiguration{ - Input: "a", - PostProcessing: resolve.PostProcessingConfiguration{ - MergePath: []string{"a"}, - }, - }, - FetchDependencies: resolve.FetchDependencies{ - FetchID: 0, - }, - }), - resolve.Single(&resolve.SingleFetch{ - FetchConfiguration: resolve.FetchConfiguration{ - Input: "b", - PostProcessing: resolve.PostProcessingConfiguration{ - MergePath: []string{"b"}, - }, - }, - FetchDependencies: resolve.FetchDependencies{ - FetchID: 1, - }, - }), - resolve.SingleWithPath(&resolve.SingleFetch{ - FetchConfiguration: resolve.FetchConfiguration{ - Input: "c", - }, - FetchDependencies: resolve.FetchDependencies{ - FetchID: 2, - DependsOnFetchIDs: []int{0}, - }, - }, "a", resolve.ObjectPath("a")), - resolve.SingleWithPath(&resolve.SingleFetch{ - FetchConfiguration: resolve.FetchConfiguration{ - Input: "d", - }, - FetchDependencies: resolve.FetchDependencies{ - FetchID: 3, - DependsOnFetchIDs: []int{1}, - }, - }, "b.c", resolve.ObjectPath("b"), resolve.ObjectPath("c")), - resolve.SingleWithPath(&resolve.SingleFetch{ - FetchConfiguration: resolve.FetchConfiguration{ - Input: "x", - }, - FetchDependencies: resolve.FetchDependencies{ - FetchID: 4, - DependsOnFetchIDs: []int{0}, - }, - }, "a", resolve.ObjectPath("a")), - resolve.Single(&resolve.SingleFetch{ - FetchConfiguration: resolve.FetchConfiguration{ - Input: "y", - PostProcessing: resolve.PostProcessingConfiguration{ - MergePath: []string{"y"}, - }, - }, - FetchDependencies: resolve.FetchDependencies{ - FetchID: 5, - }, - }), - ) - processor := &addMissingNestedDependencies{} + input := seq( + sf(0, mergePath("a")), + sf(1, mergePath("b")), + sf(2, responsePath("a")), + sf(3, responsePath("b.c")), + sf(4, responsePath("a"), dependsOn(0)), + sf(5, mergePath("y")), + ) processor.ProcessFetchTree(input) + expected := seq( + sf(0, mergePath("a")), + sf(1, mergePath("b")), + sf(2, responsePath("a"), dependsOn(0)), + sf(3, responsePath("b.c"), dependsOn(1)), + sf(4, responsePath("a"), dependsOn(0)), + sf(5, mergePath("y")), + ) require.Equal(t, expected, input) }) } diff --git a/v2/pkg/engine/postprocess/create_parallel_nodes_test.go b/v2/pkg/engine/postprocess/create_parallel_nodes_test.go index 6fb909649f..cdb559f08f 100644 --- a/v2/pkg/engine/postprocess/create_parallel_nodes_test.go +++ b/v2/pkg/engine/postprocess/create_parallel_nodes_test.go @@ -4,156 +4,154 @@ import ( "testing" "github.com/stretchr/testify/require" - - "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" ) func TestCreateParallelNodes_ProcessFetchTree(t *testing.T) { t.Run("root with 2 dependent children and one 3rd child", func(t *testing.T) { processor := &createParallelNodes{} - input := resolve.Sequence( - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 0}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 1, DependsOnFetchIDs: []int{0}}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 2, DependsOnFetchIDs: []int{0}}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 3, DependsOnFetchIDs: []int{1}}}), + input := seq( + sf(0), + sf(1, dependsOn(0)), + sf(2, dependsOn(0)), + sf(3, dependsOn(1)), ) processor.ProcessFetchTree(input) - expected := resolve.Sequence( - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 0}}), - resolve.Parallel( - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 1, DependsOnFetchIDs: []int{0}}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 2, DependsOnFetchIDs: []int{0}}}), + expected := seq( + sf(0), + par( + sf(1, dependsOn(0)), + sf(2, dependsOn(0)), ), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 3, DependsOnFetchIDs: []int{1}}}), + sf(3, dependsOn(1)), ) require.Equal(t, expected, input) }) t.Run("root with 2 dependent children and one 3rd child variant", func(t *testing.T) { processor := &createParallelNodes{} - input := resolve.Sequence( - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 0}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 1, DependsOnFetchIDs: []int{0}}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 2, DependsOnFetchIDs: []int{0}}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 3, DependsOnFetchIDs: []int{2}}}), + input := seq( + sf(0), + sf(1, dependsOn(0)), + sf(2, dependsOn(0)), + sf(3, dependsOn(2)), ) processor.ProcessFetchTree(input) - expected := resolve.Sequence( - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 0}}), - resolve.Parallel( - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 1, DependsOnFetchIDs: []int{0}}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 2, DependsOnFetchIDs: []int{0}}}), + expected := seq( + sf(0), + par( + sf(1, dependsOn(0)), + sf(2, dependsOn(0)), ), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 3, DependsOnFetchIDs: []int{2}}}), + sf(3, dependsOn(2)), ) require.Equal(t, expected, input) }) t.Run("root with 2 dependent children and one 3rd child variant 2", func(t *testing.T) { processor := &createParallelNodes{} - input := resolve.Sequence( - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 0}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 1, DependsOnFetchIDs: []int{0}}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 2, DependsOnFetchIDs: []int{0}}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 3, DependsOnFetchIDs: []int{1, 2}}}), + input := seq( + sf(0), + sf(1, dependsOn(0)), + sf(2, dependsOn(0)), + sf(3, dependsOn(1, 2)), ) processor.ProcessFetchTree(input) - expected := resolve.Sequence( - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 0}}), - resolve.Parallel( - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 1, DependsOnFetchIDs: []int{0}}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 2, DependsOnFetchIDs: []int{0}}}), + expected := seq( + sf(0), + par( + sf(1, dependsOn(0)), + sf(2, dependsOn(0)), ), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 3, DependsOnFetchIDs: []int{1, 2}}}), + sf(3, dependsOn(1, 2)), ) require.Equal(t, expected, input) }) t.Run("2 parallels depending on each other", func(t *testing.T) { processor := &createParallelNodes{} - input := resolve.Sequence( - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 0}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 1, DependsOnFetchIDs: []int{0}}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 2, DependsOnFetchIDs: []int{0}}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 3, DependsOnFetchIDs: []int{1, 2}}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 4, DependsOnFetchIDs: []int{1, 2}}}), + input := seq( + sf(0), + sf(1, dependsOn(0)), + sf(2, dependsOn(0)), + sf(3, dependsOn(1, 2)), + sf(4, dependsOn(1, 2)), ) processor.ProcessFetchTree(input) - expected := resolve.Sequence( - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 0}}), - resolve.Parallel( - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 1, DependsOnFetchIDs: []int{0}}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 2, DependsOnFetchIDs: []int{0}}}), + expected := seq( + sf(0), + par( + sf(1, dependsOn(0)), + sf(2, dependsOn(0)), ), - resolve.Parallel( - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 3, DependsOnFetchIDs: []int{1, 2}}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 4, DependsOnFetchIDs: []int{1, 2}}}), + par( + sf(3, dependsOn(1, 2)), + sf(4, dependsOn(1, 2)), ), ) require.Equal(t, expected, input) }) t.Run("2 parallels depending on each other mixed dependencies", func(t *testing.T) { processor := &createParallelNodes{} - input := resolve.Sequence( - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 0}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 1, DependsOnFetchIDs: []int{0}}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 2, DependsOnFetchIDs: []int{0}}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 3, DependsOnFetchIDs: []int{1}}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 4, DependsOnFetchIDs: []int{2}}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 5, DependsOnFetchIDs: []int{4}}}), + input := seq( + sf(0), + sf(1, dependsOn(0)), + sf(2, dependsOn(0)), + sf(3, dependsOn(1)), + sf(4, dependsOn(2)), + sf(5, dependsOn(4)), ) processor.ProcessFetchTree(input) - expected := resolve.Sequence( - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 0}}), - resolve.Parallel( - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 1, DependsOnFetchIDs: []int{0}}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 2, DependsOnFetchIDs: []int{0}}}), + expected := seq( + sf(0), + par( + sf(1, dependsOn(0)), + sf(2, dependsOn(0)), ), - resolve.Parallel( - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 3, DependsOnFetchIDs: []int{1}}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 4, DependsOnFetchIDs: []int{2}}}), + par( + sf(3, dependsOn(1)), + sf(4, dependsOn(2)), ), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 5, DependsOnFetchIDs: []int{4}}}), + sf(5, dependsOn(4)), ) require.Equal(t, expected, input) }) t.Run("2 parallels with single in the middle", func(t *testing.T) { processor := &createParallelNodes{} - input := resolve.Sequence( - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 0}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 1, DependsOnFetchIDs: []int{0}}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 2, DependsOnFetchIDs: []int{0}}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 3, DependsOnFetchIDs: []int{1, 2}}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 4, DependsOnFetchIDs: []int{1, 3}}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 5, DependsOnFetchIDs: []int{2, 3}}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 6, DependsOnFetchIDs: []int{4, 5}}}), + input := seq( + sf(0), + sf(1, dependsOn(0)), + sf(2, dependsOn(0)), + sf(3, dependsOn(1, 2)), + sf(4, dependsOn(1, 3)), + sf(5, dependsOn(2, 3)), + sf(6, dependsOn(4, 5)), ) processor.ProcessFetchTree(input) - expected := resolve.Sequence( - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 0}}), - resolve.Parallel( - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 1, DependsOnFetchIDs: []int{0}}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 2, DependsOnFetchIDs: []int{0}}}), + expected := seq( + sf(0), + par( + sf(1, dependsOn(0)), + sf(2, dependsOn(0)), ), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 3, DependsOnFetchIDs: []int{1, 2}}}), - resolve.Parallel( - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 4, DependsOnFetchIDs: []int{1, 3}}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 5, DependsOnFetchIDs: []int{2, 3}}}), + sf(3, dependsOn(1, 2)), + par( + sf(4, dependsOn(1, 3)), + sf(5, dependsOn(2, 3)), ), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 6, DependsOnFetchIDs: []int{4, 5}}}), + sf(6, dependsOn(4, 5)), ) require.Equal(t, expected, input) }) t.Run("3 fetches in parallel without dependencies", func(t *testing.T) { processor := &createParallelNodes{} - input := resolve.Sequence( - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 0}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 1}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 2}}), + input := seq( + sf(0), + sf(1), + sf(2), ) processor.ProcessFetchTree(input) - expected := resolve.Sequence( - resolve.Parallel( - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 0}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 1}}), - resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: 2}}), + expected := seq( + par( + sf(0), + sf(1), + sf(2), ), ) require.Equal(t, expected, input) diff --git a/v2/pkg/engine/postprocess/order_sequence_by_dependencies.go b/v2/pkg/engine/postprocess/order_sequence_by_dependencies.go index 45f247eb24..06adb6e44e 100644 --- a/v2/pkg/engine/postprocess/order_sequence_by_dependencies.go +++ b/v2/pkg/engine/postprocess/order_sequence_by_dependencies.go @@ -61,15 +61,8 @@ func (o *orderSequenceByDependencies) nodeDependsOn(node, root *resolve.FetchTre result = append(result, o.nodeDependsOn(child, root)...) } } - index := make(map[int]struct{}, len(result)) - for _, id := range result { - index[id] = struct{}{} - } - result = make([]int, 0, len(index)) - for id := range index { - result = append(result, id) - } slices.Sort(result) + result = slices.Compact(result) return result } diff --git a/v2/pkg/engine/postprocess/order_sequence_by_dependencies_test.go b/v2/pkg/engine/postprocess/order_sequence_by_dependencies_test.go index 10a85b8b43..d8f541ea1c 100644 --- a/v2/pkg/engine/postprocess/order_sequence_by_dependencies_test.go +++ b/v2/pkg/engine/postprocess/order_sequence_by_dependencies_test.go @@ -1,169 +1,191 @@ package postprocess import ( - "encoding/json" "testing" "github.com/stretchr/testify/require" - - "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" ) -func sequenceToDeps(seq *resolve.FetchTreeNode) []resolve.FetchDependencies { - result := make([]resolve.FetchDependencies, len(seq.ChildNodes)) - for i, node := range seq.ChildNodes { - result[i] = node.Item.Fetch.(*resolve.SingleFetch).FetchDependencies - } - return result -} - -func depsToSequence(deps []resolve.FetchDependencies) *resolve.FetchTreeNode { - result := &resolve.FetchTreeNode{ - ChildNodes: make([]*resolve.FetchTreeNode, len(deps)), - } - for i, dep := range deps { - result.ChildNodes[i] = &resolve.FetchTreeNode{ - Kind: resolve.FetchTreeNodeKindSingle, - Item: &resolve.FetchItem{ - Fetch: &resolve.SingleFetch{FetchDependencies: dep}, - }, - } - } - return result -} - -func prettyPrint(input any) string { - out, _ := json.MarshalIndent(input, "", " ") - return string(out) -} - -func TestOrderSquenceByDependencies_ProcessFetchTree(t *testing.T) { - processor := &orderSequenceByDependencies{} +func TestOrderSequenceByDependencies_ProcessFetchTree(t *testing.T) { t.Run("no dependencies", func(t *testing.T) { - input := []resolve.FetchDependencies{ - {FetchID: 2}, - {FetchID: 0}, - {FetchID: 1}, - } - expected := []resolve.FetchDependencies{ - {FetchID: 0}, - {FetchID: 1}, - {FetchID: 2}, - } - seq := depsToSequence(input) - processor.ProcessFetchTree(seq) - require.Equal(t, prettyPrint(expected), prettyPrint(sequenceToDeps(seq))) + processor := &orderSequenceByDependencies{} + input := seq( + sf(2), + sf(0), + sf(1), + ) + processor.ProcessFetchTree(input) + expected := seq( + sf(0), + sf(1), + sf(2), + ) + require.Equal(t, expected, input) }) t.Run("serial dependencies", func(t *testing.T) { - input := []resolve.FetchDependencies{ - {FetchID: 0}, - {FetchID: 2, DependsOnFetchIDs: []int{1}}, - {FetchID: 1, DependsOnFetchIDs: []int{0}}, - } - expected := []resolve.FetchDependencies{ - {FetchID: 0}, - {FetchID: 1, DependsOnFetchIDs: []int{0}}, - {FetchID: 2, DependsOnFetchIDs: []int{1}}, - } - seq := depsToSequence(input) - processor.ProcessFetchTree(seq) - require.Equal(t, prettyPrint(expected), prettyPrint(sequenceToDeps(seq))) + processor := &orderSequenceByDependencies{} + input := seq( + sf(0), + sf(2, dependsOn(1)), + sf(1, dependsOn(0)), + ) + processor.ProcessFetchTree(input) + expected := seq( + sf(0), + sf(1, dependsOn(0)), + sf(2, dependsOn(1)), + ) + require.Equal(t, expected, input) }) t.Run("serial + requires dependencies", func(t *testing.T) { - input := []resolve.FetchDependencies{ - {FetchID: 0}, - {FetchID: 1, DependsOnFetchIDs: []int{0, 2}}, - {FetchID: 2, DependsOnFetchIDs: []int{0}}, - } - expected := []resolve.FetchDependencies{ - {FetchID: 0}, - {FetchID: 2, DependsOnFetchIDs: []int{0}}, - {FetchID: 1, DependsOnFetchIDs: []int{0, 2}}, - } - seq := depsToSequence(input) - processor.ProcessFetchTree(seq) - require.Equal(t, prettyPrint(expected), prettyPrint(sequenceToDeps(seq))) + processor := &orderSequenceByDependencies{} + input := seq( + sf(0), + sf(1, dependsOn(0, 2)), + sf(2, dependsOn(0)), + ) + processor.ProcessFetchTree(input) + expected := seq( + sf(0), + sf(2, dependsOn(0)), + sf(1, dependsOn(0, 2)), + ) + require.Equal(t, expected, input) }) t.Run("more dependencies", func(t *testing.T) { - input := []resolve.FetchDependencies{ - {FetchID: 4, DependsOnFetchIDs: []int{3}}, - {FetchID: 0, DependsOnFetchIDs: []int{}}, - {FetchID: 2, DependsOnFetchIDs: []int{1}}, - {FetchID: 3, DependsOnFetchIDs: []int{5, 1}}, - {FetchID: 1, DependsOnFetchIDs: []int{0}}, - {FetchID: 5, DependsOnFetchIDs: []int{0}}, - } - expected := []resolve.FetchDependencies{ - {FetchID: 0, DependsOnFetchIDs: []int{}}, - {FetchID: 1, DependsOnFetchIDs: []int{0}}, - {FetchID: 5, DependsOnFetchIDs: []int{0}}, - {FetchID: 2, DependsOnFetchIDs: []int{1}}, - {FetchID: 3, DependsOnFetchIDs: []int{5, 1}}, - {FetchID: 4, DependsOnFetchIDs: []int{3}}, - } - seq := depsToSequence(input) - processor.ProcessFetchTree(seq) - require.Equal(t, prettyPrint(expected), prettyPrint(sequenceToDeps(seq))) + processor := &orderSequenceByDependencies{} + input := seq( + sf(4, dependsOn(3)), + sf(0), + sf(2, dependsOn(1)), + sf(3, dependsOn(5, 1)), + sf(1, dependsOn(0)), + sf(5, dependsOn(0)), + ) + processor.ProcessFetchTree(input) + expected := seq( + sf(0), + sf(1, dependsOn(0)), + sf(5, dependsOn(0)), + sf(2, dependsOn(1)), + sf(3, dependsOn(5, 1)), + sf(4, dependsOn(3)), + ) + require.Equal(t, expected, input) }) t.Run("double dependencies", func(t *testing.T) { - input := []resolve.FetchDependencies{ - {FetchID: 0, DependsOnFetchIDs: []int{}}, - {FetchID: 1, DependsOnFetchIDs: []int{0}}, - {FetchID: 2, DependsOnFetchIDs: []int{0, 5}}, - {FetchID: 3, DependsOnFetchIDs: []int{0, 1}}, - {FetchID: 4, DependsOnFetchIDs: []int{2}}, - {FetchID: 5, DependsOnFetchIDs: []int{0}}, - } - expected := []resolve.FetchDependencies{ - {FetchID: 0, DependsOnFetchIDs: []int{}}, - {FetchID: 1, DependsOnFetchIDs: []int{0}}, - {FetchID: 5, DependsOnFetchIDs: []int{0}}, - {FetchID: 2, DependsOnFetchIDs: []int{0, 5}}, - {FetchID: 3, DependsOnFetchIDs: []int{0, 1}}, - {FetchID: 4, DependsOnFetchIDs: []int{2}}, - } - seq := depsToSequence(input) - processor.ProcessFetchTree(seq) - require.Equal(t, prettyPrint(expected), prettyPrint(sequenceToDeps(seq))) + processor := &orderSequenceByDependencies{} + input := seq( + sf(0), + sf(1, dependsOn(0)), + sf(2, dependsOn(0, 5)), + sf(3, dependsOn(0, 1)), + sf(4, dependsOn(2)), + sf(5, dependsOn(0)), + ) + processor.ProcessFetchTree(input) + expected := seq( + sf(0), + sf(1, dependsOn(0)), + sf(5, dependsOn(0)), + sf(2, dependsOn(0, 5)), + sf(3, dependsOn(0, 1)), + sf(4, dependsOn(2)), + ) + require.Equal(t, expected, input) }) t.Run("double dependencies variant", func(t *testing.T) { - input := []resolve.FetchDependencies{ - {FetchID: 0, DependsOnFetchIDs: []int{}}, - {FetchID: 2, DependsOnFetchIDs: []int{0, 1}}, - {FetchID: 1, DependsOnFetchIDs: []int{0}}, - {FetchID: 3, DependsOnFetchIDs: []int{2}}, - {FetchID: 5, DependsOnFetchIDs: []int{4}}, - {FetchID: 4, DependsOnFetchIDs: []int{2, 3}}, - } - expected := []resolve.FetchDependencies{ - {FetchID: 0, DependsOnFetchIDs: []int{}}, - {FetchID: 1, DependsOnFetchIDs: []int{0}}, - {FetchID: 2, DependsOnFetchIDs: []int{0, 1}}, - {FetchID: 3, DependsOnFetchIDs: []int{2}}, - {FetchID: 4, DependsOnFetchIDs: []int{2, 3}}, - {FetchID: 5, DependsOnFetchIDs: []int{4}}, - } - seq := depsToSequence(input) - processor.ProcessFetchTree(seq) - require.Equal(t, prettyPrint(expected), prettyPrint(sequenceToDeps(seq))) + processor := &orderSequenceByDependencies{} + input := seq( + sf(0), + sf(2, dependsOn(0, 1)), + sf(1, dependsOn(0)), + sf(3, dependsOn(2)), + sf(5, dependsOn(4)), + sf(4, dependsOn(2, 3)), + ) + processor.ProcessFetchTree(input) + expected := seq( + sf(0), + sf(1, dependsOn(0)), + sf(2, dependsOn(0, 1)), + sf(3, dependsOn(2)), + sf(4, dependsOn(2, 3)), + sf(5, dependsOn(4)), + ) + require.Equal(t, expected, input) }) t.Run("nested requires", func(t *testing.T) { - input := []resolve.FetchDependencies{ - {FetchID: 0, DependsOnFetchIDs: []int{}}, - {FetchID: 3, DependsOnFetchIDs: []int{0, 2}}, - {FetchID: 1, DependsOnFetchIDs: []int{0}}, - {FetchID: 2, DependsOnFetchIDs: []int{0}}, - {FetchID: 4, DependsOnFetchIDs: []int{0, 1}}, - } - expected := []resolve.FetchDependencies{ - {FetchID: 0, DependsOnFetchIDs: []int{}}, - {FetchID: 1, DependsOnFetchIDs: []int{0}}, - {FetchID: 2, DependsOnFetchIDs: []int{0}}, - {FetchID: 3, DependsOnFetchIDs: []int{0, 2}}, - {FetchID: 4, DependsOnFetchIDs: []int{0, 1}}, - } - seq := depsToSequence(input) - processor.ProcessFetchTree(seq) - require.Equal(t, prettyPrint(expected), prettyPrint(sequenceToDeps(seq))) + processor := &orderSequenceByDependencies{} + input := seq( + sf(0), + sf(3, dependsOn(0, 2)), + sf(1, dependsOn(0)), + sf(2, dependsOn(0)), + sf(4, dependsOn(0, 1)), + ) + processor.ProcessFetchTree(input) + expected := seq( + sf(0), + sf(1, dependsOn(0)), + sf(2, dependsOn(0)), + sf(3, dependsOn(0, 2)), + sf(4, dependsOn(0, 1)), + ) + require.Equal(t, expected, input) + }) + + t.Run("dependent with fetch ID 0 must come after its dependency", func(t *testing.T) { + processor := &orderSequenceByDependencies{} + input := seq( + sf(0, dependsOn(3)), + sf(3, dependsOn(1, 2)), + sf(1, dependsOn(5)), + sf(2, dependsOn(5)), + sf(5), + ) + processor.ProcessFetchTree(input) + expected := seq( + sf(5), + sf(1, dependsOn(5)), + sf(2, dependsOn(5)), + sf(3, dependsOn(1, 2)), + sf(0, dependsOn(3)), + ) + require.Equal(t, expected, input) + }) + t.Run("equal transitive dependencies tie-break by fetch ID (diamond)", func(t *testing.T) { + processor := &orderSequenceByDependencies{} + input := seq( + sf(7, dependsOn(4, 5)), + sf(6, dependsOn(3, 4, 5)), + sf(3), + sf(4, dependsOn(3)), + sf(5, dependsOn(3)), + ) + processor.ProcessFetchTree(input) + expected := seq( + sf(3), + sf(4, dependsOn(3)), + sf(5, dependsOn(3)), + sf(6, dependsOn(3, 4, 5)), + sf(7, dependsOn(4, 5)), + ) + require.Equal(t, expected, input) + }) + t.Run("duplicate direct dependency IDs tie-break by fetch ID", func(t *testing.T) { + processor := &orderSequenceByDependencies{} + input := seq( + sf(3, dependsOn(1)), + sf(2, dependsOn(1, 1)), + sf(1), + ) + processor.ProcessFetchTree(input) + expected := seq( + sf(1), + sf(2, dependsOn(1, 1)), + sf(3, dependsOn(1)), + ) + require.Equal(t, expected, input) }) } diff --git a/v2/pkg/engine/postprocess/postprocess.go b/v2/pkg/engine/postprocess/postprocess.go index b6d7b91a8b..f24d258a1a 100644 --- a/v2/pkg/engine/postprocess/postprocess.go +++ b/v2/pkg/engine/postprocess/postprocess.go @@ -38,6 +38,7 @@ type FetchTreeProcessors struct { createConcreteSingleFetchTypes *createConcreteSingleFetchTypes orderSequenceByDependencies *orderSequenceByDependencies createParallelNodes *createParallelNodes + scheduleFetches *scheduleFetches } // processFlatFetchTree runs the stages that operate on the still-flat fetch tree @@ -55,19 +56,70 @@ func (p *FetchTreeProcessors) processFlatFetchTree(response *resolve.GraphQLResp p.addMissingNestedDependencies.ProcessFetchTree(fetches) } -// organizeFetchTree organizes the fetch tree by ordering sequence nodes by dependencies and creating parallel nodes. -// after this step fetches have tree structure of serial and parallel nodes. +// organizeFetchTree merges same-subgraph entity fetches and organizes the tree into +// a nested Parallel/Sequence structure, either via the scheduler or +// as a Sequence of waves (Parallel groups) when the scheduler is disabled func (p *FetchTreeProcessors) organizeFetchTree(fetches *resolve.FetchTreeNode) { + merged := false + if !p.scheduleFetches.disable && fetches != nil && fetches.Kind == resolve.FetchTreeNodeKindSequence { + if !p.createMultiFetch.disable { + // Merge-before-schedule: materialize the legacy wave structure only + // to discover maximal same-wave merge groups, merge them, + // then drop the wave structure and schedule the merged DAG. + p.organizeFetchTreeInWaves(fetches) + p.createMultiFetch.ProcessFetchTree(fetches) + flattenFetchTree(fetches) + merged = true + } + if err := p.scheduleFetches.ProcessFetchTree(fetches); err == nil { + return + } + } + p.organizeFetchTreeInWaves(fetches) + if !merged { + p.createMultiFetch.ProcessFetchTree(fetches) + } +} + +func (p *FetchTreeProcessors) organizeFetchTreeInWaves(fetches *resolve.FetchTreeNode) { p.orderSequenceByDependencies.ProcessFetchTree(fetches) p.createParallelNodes.ProcessFetchTree(fetches) } -// processOrganizedFetchTree runs the stages that operate on the tree organized in waves. +// flattenFetchTree collapses an organized tree back into the flat Sequence shape +// the scheduler consumes: the root keeps its identity (Trigger, NormalizedQuery) +// and its children become the in-order list of Single nodes. In-order collection +// of a dependency-ordered tree is already topologically sorted, which the legacy +// fallback inside scheduleFetches relies on. +func flattenFetchTree(root *resolve.FetchTreeNode) { + if root == nil || root.Kind != resolve.FetchTreeNodeKindSequence { + return + } + var singles []*resolve.FetchTreeNode + var walk func(node *resolve.FetchTreeNode) + walk = func(node *resolve.FetchTreeNode) { + if node == nil { + return + } + if node.Kind == resolve.FetchTreeNodeKindSingle { + singles = append(singles, node) + return + } + for _, child := range node.ChildNodes { + walk(child) + } + } + for _, child := range root.ChildNodes { + walk(child) + } + root.ChildNodes = singles +} + +// processOrganizedFetchTree runs the stages that operate on the ORGANIZED tree, +// after organizeFetchTree has materialized the real sequence/parallel waves. +// It is applied to every response tree, including each extracted defer group, +// so that the same semantics hold for deferred fetches. func (p *FetchTreeProcessors) processOrganizedFetchTree(fetches *resolve.FetchTreeNode) { - // merge same-subgraph entity fetches within each real parallel group when enabled. - p.createMultiFetch.ProcessFetchTree(fetches) - // render the deferred entity-fetch input string for unmerged fetches and - // clear the SubgraphOperation artifacts so no AST survives postprocessing. p.renderSubgraphInputs.ProcessFetchTree(fetches) p.resolveInputTemplates.ProcessFetchTree(fetches) p.createConcreteSingleFetchTypes.ProcessFetchTree(fetches) @@ -92,6 +144,14 @@ type processorOptions struct { disableBuildDeferTree bool disableCollectAuthorizationCoordinates bool enableMultiFetch bool + enableScheduleFetches bool +} + +// EnableScheduleFetches activates the nested schedule-tree scheduler. +func EnableScheduleFetches() ProcessorOption { + return func(o *processorOptions) { + o.enableScheduleFetches = true + } } type ProcessorOption func(*processorOptions) @@ -216,6 +276,9 @@ func NewProcessor(options ...ProcessorOption) *Processor { createParallelNodes: &createParallelNodes{ disable: opts.disableCreateParallelNodes, }, + scheduleFetches: &scheduleFetches{ + disable: !opts.enableScheduleFetches, + }, }, responseTreeProcessors: &ResponseTreeProcessors{ mergeFields: &mergeFields{ @@ -299,7 +362,7 @@ func (p *Processor) createFetchTree(res *resolve.GraphQLResponse) { children := make([]*resolve.FetchTreeNode, len(fetches)) if p.collectDataSourceInfo { - var list = make([]resolve.DataSourceInfo, 0, len(fetches)) + list := make([]resolve.DataSourceInfo, 0, len(fetches)) for _, fetch := range fetches { info := fetch.Fetch.FetchInfo() if info != nil { diff --git a/v2/pkg/engine/postprocess/resolve_input_templates_test.go b/v2/pkg/engine/postprocess/resolve_input_templates_test.go index 4c0f5973dd..9b42c99a8f 100644 --- a/v2/pkg/engine/postprocess/resolve_input_templates_test.go +++ b/v2/pkg/engine/postprocess/resolve_input_templates_test.go @@ -342,7 +342,13 @@ func TestDataSourceInput_Process(t *testing.T) { }, } - processor := NewProcessor(DisableMergeFields(), DisableDeduplicateSingleFetches(), DisableCreateConcreteSingleFetchTypes(), DisableCreateParallelNodes(), DisableAddMissingNestedDependencies()) + processor := NewProcessor( + DisableMergeFields(), + DisableDeduplicateSingleFetches(), + DisableCreateConcreteSingleFetchTypes(), + DisableCreateParallelNodes(), + DisableAddMissingNestedDependencies(), + ) processor.Process(pre) assert.Equal(t, expected, pre) @@ -454,7 +460,12 @@ func TestDataSourceInput_ProcessTrigger(t *testing.T) { }, } - processor := NewProcessor(DisableMergeFields(), DisableDeduplicateSingleFetches(), DisableCreateConcreteSingleFetchTypes(), DisableAddMissingNestedDependencies()) + processor := NewProcessor( + DisableMergeFields(), + DisableDeduplicateSingleFetches(), + DisableCreateConcreteSingleFetchTypes(), + DisableAddMissingNestedDependencies(), + ) processor.Process(pre) if !assert.Equal(t, expected, pre) { diff --git a/v2/pkg/engine/postprocess/schedule_fetches.go b/v2/pkg/engine/postprocess/schedule_fetches.go new file mode 100644 index 0000000000..f38e9dd31f --- /dev/null +++ b/v2/pkg/engine/postprocess/schedule_fetches.go @@ -0,0 +1,506 @@ +package postprocess + +import ( + "cmp" + "fmt" + "math" + "slices" + + "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" +) + +// scheduleFetches is a dependency-aware scheduler that emits nested Sequence/Parallel trees, +// collapsing independent chains onto their own branches instead of synchronizing them at wave +// barriers. +// +// This scheduler tries to break the top level DAG into weakly connected components. +// For each component it picks between two strategies of scheduling: +// - the waves tree, that barriers all ready roots per step, +// - the inlined tree, that pulls each root's exclusively-reachable descendants into that root's branch. +// +// The inlined tree wins only when a per-fetch predecessor-containment proof shows it can +// never be slower than the component's waves tree, no matter how long each fetch takes. +// Since components never wait on each other, mixing winners is always safe. +// +// On any scheduler or validator error the processor falls back to the legacy wave pipeline. +type scheduleFetches struct { + disable bool +} + +func (b *scheduleFetches) ProcessFetchTree(root *resolve.FetchTreeNode) error { + dag, err := newFetchDAG(root.ChildNodes) + if err != nil { + return err + } + tree, err := buildScheduleTree(root.ChildNodes, dag) + if err != nil { + return err + } + if tree == nil { + root.ChildNodes = nil + return nil + } + // A subscription root must stay a Sequence: the plan printer renders the + // Subscription Primary/Rest wrapper only for Sequence roots, so the tree + // must not collapse into the root when it carries a Trigger. + if root.Trigger != nil && tree.Kind != resolve.FetchTreeNodeKindSequence { + tree = resolve.Sequence(tree) + } + // Replace only the scheduling-related fields: the root may carry a + // subscription Trigger or NormalizedQuery that must survive rescheduling. + root.Kind = tree.Kind + root.Item = tree.Item + root.ChildNodes = tree.ChildNodes + return nil +} + +type fetchDAG struct { + nodes map[int]*resolve.FetchTreeNode + parents map[int]map[int]struct{} + children map[int]map[int]struct{} +} + +func newFetchDAG(nodes []*resolve.FetchTreeNode) (*fetchDAG, error) { + dag := &fetchDAG{ + nodes: make(map[int]*resolve.FetchTreeNode, len(nodes)), + parents: make(map[int]map[int]struct{}, len(nodes)), + children: make(map[int]map[int]struct{}, len(nodes)), + } + for _, node := range nodes { + if node == nil || node.Item == nil || node.Item.Fetch == nil { + return nil, fmt.Errorf("non-single node in flat fetch sequence") + } + id := node.Item.Fetch.Dependencies().FetchID + if _, exists := dag.nodes[id]; exists { + return nil, fmt.Errorf("duplicate fetch id %d", id) + } + dag.nodes[id] = node + dag.parents[id] = map[int]struct{}{} + dag.children[id] = map[int]struct{}{} + } + for id, node := range dag.nodes { + for _, dep := range node.Item.Fetch.Dependencies().DependsOnFetchIDs { + if _, exists := dag.nodes[dep]; !exists { + continue // dependency satisfied outside this tree + } + if id == dep { + return nil, fmt.Errorf("self-dependent id %d", id) + } + dag.parents[id][dep] = struct{}{} + dag.children[dep][id] = struct{}{} + } + } + return dag, nil +} + +func buildScheduleTree(roots []*resolve.FetchTreeNode, dag *fetchDAG) (*resolve.FetchTreeNode, error) { + ids := make([]int, 0, len(roots)) + for _, root := range roots { + if root == nil || root.Item == nil || root.Item.Fetch == nil { + continue + } + ids = append(ids, root.Item.Fetch.Dependencies().FetchID) + } + // Pick the best strategy on the top level for weakly connected trees. + components := weaklyConnectedComponents(sortedCopy(ids), dag) + winners := make([]*resolve.FetchTreeNode, 0, len(components)) + for _, component := range components { + waves, err := schedule(component, dag, false) + if err != nil { + return nil, err + } + inlined, err := schedule(component, dag, true) + if err != nil { + return nil, err + } + if dominates(inlined, waves) { + winners = append(winners, inlined) + } else { + winners = append(winners, waves) + } + } + winner := parallelOf(winners) + if err := validateSchedule(winner, dag); err != nil { + return nil, err + } + return winner, nil +} + +// schedule builds a fetch tree for the DAG restricted to set. +// Dependencies on fetches outside set are treated as already satisfied. +// Weakly connected components run in Parallel; +// within a component the ready roots run in Parallel followed by the remainder in Sequence. +// When inlined, each root pulls the descendants reachable only through it into its branch. +func schedule(set []int, dag *fetchDAG, inline bool) (*resolve.FetchTreeNode, error) { + sortedSet := sortedCopy(set) + switch len(sortedSet) { + case 0: + return nil, nil + case 1: + return dag.nodes[sortedSet[0]], nil + } + components := weaklyConnectedComponents(sortedSet, dag) + if len(components) > 1 { + branches := make([]*resolve.FetchTreeNode, 0, len(components)) + for _, component := range components { + child, err := schedule(component, dag, inline) + if err != nil { + return nil, err + } + branches = append(branches, child) + } + return parallelOf(branches), nil + } + // Find the ready fetches. + // Roots have no parents in inSet; their dependencies outside inSet complete + // before this subtree starts, so they are ready to run as the first wave. + inSet := asMap(sortedSet) + roots := make([]int, 0, len(sortedSet)) + for _, id := range sortedSet { + if !hasParentIn(dag, id, inSet) { + roots = append(roots, id) + } + } + if len(roots) == 0 { + return nil, fmt.Errorf("cycle detected in fetch dependency graph") + } + + // A fetch reachable from exactly one root cannot start before that root finishes, + // so inlining it into the root's branch never delays it and frees it from waiting on sibling roots. + scheduled := asMap(roots) + // exclusive maps each root to the fetches reachable only from that root. + exclusive := map[int][]int{} + if inline { + for id, root := range colorExclusive(roots, dag, inSet) { + if root == sharedByRoots { + // Fetch stays in the caller's rest set. + continue + } + exclusive[root] = append(exclusive[root], id) + scheduled[id] = struct{}{} + } + } + branches := make([]*resolve.FetchTreeNode, 0, len(roots)) + for _, root := range roots { + branch := dag.nodes[root] + if members := exclusive[root]; len(members) > 0 { + subtree, err := schedule(members, dag, inline) + if err != nil { + return nil, err + } + branch = sequenceOf([]*resolve.FetchTreeNode{branch, subtree}) + } + branches = append(branches, branch) + } + + rest := make([]int, 0, len(sortedSet)-len(scheduled)) + for _, id := range sortedSet { + if _, ok := scheduled[id]; !ok { + rest = append(rest, id) + } + } + restTree, err := schedule(rest, dag, inline) + if err != nil { + return nil, err + } + return sequenceOf([]*resolve.FetchTreeNode{parallelOf(branches), restTree}), nil +} + +func hasParentIn(dag *fetchDAG, id int, set map[int]struct{}) bool { + for parent := range dag.parents[id] { + if _, ok := set[parent]; ok { + return true + } + } + return false +} + +// sharedByRoots marks a fetch reachable from more than one root. Fetch IDs are non-negative. +const sharedByRoots = -1 + +// colorExclusive labels every fetch of the component reachable from a root with that root's id, +// or with sharedByRoots when several roots reach it. +// It returns the mapping from fetch ID to the single root it's reachable from, or sharedByRoots. +func colorExclusive(roots []int, dag *fetchDAG, inSet map[int]struct{}) map[int]int { + type item struct { + id, color int + } + queue := make([]item, 0, len(inSet)) + for _, root := range roots { + for child := range dag.children[root] { + queue = append(queue, item{child, root}) + } + } + color := make(map[int]int, len(inSet)) + for len(queue) > 0 { + next := queue[0] + queue = queue[1:] + if _, ok := inSet[next.id]; !ok { + // A shared dependant of an enclosing component; its "rest" set runs after this subtree. + continue + } + current, seen := color[next.id] + if seen && (current == next.color || current == sharedByRoots) { + continue + } + if seen { + next.color = sharedByRoots + } + color[next.id] = next.color + for child := range dag.children[next.id] { + queue = append(queue, item{child, next.color}) + } + } + return color +} + +// dominates checks whether treeA is never slower than treeB, no matter how long each fetch takes. +// +// The test: every fetch's predecessor set in A must be a subset of its predecessor set in B. +// The fetches on any critical path of A are pairwise ordered in A, hence also in B, +// so they form a chain in B and B's makespan is at least that chain's weight — a's makespan. +// +// The test is also exact, not just sufficient: given any extra predecessor pair in A, +// there are fetch durations for which A is strictly slower. +func dominates(treeA, treeB *resolve.FetchTreeNode) bool { + predA, predB := treePredecessors(treeA), treePredecessors(treeB) + if len(predA) != len(predB) { + return false + } + for id, pa := range predA { + pb, exists := predB[id] + if !exists { + return false + } + for waitID := range pa { + if _, ok := pb[waitID]; !ok { + return false + } + } + } + return true +} + +// treePredecessors returns, per fetch, the set of fetches the tree guarantees +// to have completed before that fetch starts. +func treePredecessors(root *resolve.FetchTreeNode) map[int]map[int]struct{} { + preds := map[int]map[int]struct{}{} + var walk func(node *resolve.FetchTreeNode, before map[int]struct{}) []int + walk = func(node *resolve.FetchTreeNode, before map[int]struct{}) []int { + if node == nil { + return nil + } + switch node.Kind { + case resolve.FetchTreeNodeKindSingle: + id := node.Item.Fetch.Dependencies().FetchID + set := make(map[int]struct{}, len(before)) + for k := range before { + set[k] = struct{}{} + } + preds[id] = set + return []int{id} + case resolve.FetchTreeNodeKindParallel: + var ids []int + for _, child := range node.ChildNodes { + ids = append(ids, walk(child, before)...) + } + return ids + case resolve.FetchTreeNodeKindSequence: + acc := make(map[int]struct{}, len(before)) + for k := range before { + acc[k] = struct{}{} + } + var ids []int + for _, child := range node.ChildNodes { + childIDs := walk(child, acc) + for _, id := range childIDs { + acc[id] = struct{}{} + } + ids = append(ids, childIDs...) + } + return ids + default: + return nil + } + } + walk(root, map[int]struct{}{}) + return preds +} + +// validateSchedule walks the tree once, checking that every fetch's declared +// dependencies are sequenced strictly before it, and that the tree contains +// every DAG fetch exactly once (a schedule must never lose or duplicate work). +func validateSchedule(root *resolve.FetchTreeNode, dag *fetchDAG) error { + seen := make(map[int]int, len(dag.nodes)) + var walk func(node *resolve.FetchTreeNode, before map[int]struct{}) ([]int, error) + walk = func(node *resolve.FetchTreeNode, before map[int]struct{}) ([]int, error) { + if node == nil { + return nil, nil + } + switch node.Kind { + case resolve.FetchTreeNodeKindSingle: + id := node.Item.Fetch.Dependencies().FetchID + if _, ok := dag.nodes[id]; !ok { + return nil, fmt.Errorf("fetch %d not found in dag", id) + } + seen[id]++ + for _, dep := range node.Item.Fetch.Dependencies().DependsOnFetchIDs { + if _, known := dag.nodes[dep]; !known { + continue + } + if _, ok := before[dep]; !ok { + return nil, fmt.Errorf("fetch %d is scheduled before its dependency %d completes", id, dep) + } + } + return []int{id}, nil + case resolve.FetchTreeNodeKindParallel: + var ids []int + for _, child := range node.ChildNodes { + childIDs, err := walk(child, before) + if err != nil { + return nil, err + } + ids = append(ids, childIDs...) + } + return ids, nil + case resolve.FetchTreeNodeKindSequence: + available := make(map[int]struct{}, len(before)) + for k := range before { + available[k] = struct{}{} + } + var ids []int + for _, child := range node.ChildNodes { + childIDs, err := walk(child, available) + if err != nil { + return nil, err + } + for _, id := range childIDs { + available[id] = struct{}{} + } + ids = append(ids, childIDs...) + } + return ids, nil + default: + return nil, fmt.Errorf("unexpected node kind %q in schedule", node.Kind) + } + } + if _, err := walk(root, map[int]struct{}{}); err != nil { + return err + } + for id, count := range seen { + if count > 1 { + return fmt.Errorf("fetch %d scheduled %d times", id, count) + } + } + for id := range dag.nodes { + if seen[id] == 0 { + return fmt.Errorf("fetch %d missing from schedule", id) + } + } + return nil +} + +// weaklyConnectedComponents returns a slice of weakly connected components containing +// nodes which is an ordered set. +func weaklyConnectedComponents(nodes []int, dag *fetchDAG) [][]int { + allowed := asMap(nodes) + seen := map[int]struct{}{} + components := make([][]int, 0) + for _, id := range nodes { + if _, ok := seen[id]; ok { + continue + } + queue := []int{id} + seen[id] = struct{}{} + component := make([]int, 0) + for len(queue) != 0 { + current := queue[0] + queue = queue[1:] + component = append(component, current) + for _, neighbors := range []map[int]struct{}{dag.parents[current], dag.children[current]} { + for neighbor := range neighbors { + if _, ok := allowed[neighbor]; !ok { + continue + } + if _, ok := seen[neighbor]; ok { + continue + } + seen[neighbor] = struct{}{} + queue = append(queue, neighbor) + } + } + } + slices.Sort(component) + components = append(components, component) + } + return components +} + +// sequenceOf and parallelOf normalize child lists into fresh slices: +// nils dropped, same-kind children spliced inline, singleton unwrapped. +func sequenceOf(children []*resolve.FetchTreeNode) *resolve.FetchTreeNode { + return combineOf(resolve.FetchTreeNodeKindSequence, children) +} + +func parallelOf(children []*resolve.FetchTreeNode) *resolve.FetchTreeNode { + return combineOf(resolve.FetchTreeNodeKindParallel, children) +} + +func combineOf(kind resolve.FetchTreeNodeKind, children []*resolve.FetchTreeNode) *resolve.FetchTreeNode { + out := make([]*resolve.FetchTreeNode, 0, len(children)) + for _, child := range children { + switch { + case child == nil: + case child.Kind == kind: + out = append(out, child.ChildNodes...) + default: + out = append(out, child) + } + } + switch len(out) { + case 0: + return nil + case 1: + return out[0] + } + if kind == resolve.FetchTreeNodeKindParallel { + slices.SortFunc(out, func(a, b *resolve.FetchTreeNode) int { + return cmp.Compare(minReachableFetchID(a), minReachableFetchID(b)) + }) + return resolve.Parallel(out...) + } + return resolve.Sequence(out...) +} + +func minReachableFetchID(node *resolve.FetchTreeNode) int { + minID := math.MaxInt + if node == nil { + return minID + } + if node.Kind == resolve.FetchTreeNodeKindSingle { + return node.Item.Fetch.Dependencies().FetchID + } + for _, child := range node.ChildNodes { + if childMin := minReachableFetchID(child); childMin < minID { + minID = childMin + } + } + return minID +} + +func sortedCopy(ids []int) []int { + if len(ids) == 0 { + return nil + } + out := append([]int{}, ids...) + slices.Sort(out) + return out +} + +func asMap(ids []int) map[int]struct{} { + set := make(map[int]struct{}, len(ids)) + for _, id := range ids { + set[id] = struct{}{} + } + return set +} diff --git a/v2/pkg/engine/postprocess/schedule_fetches_test.go b/v2/pkg/engine/postprocess/schedule_fetches_test.go new file mode 100644 index 0000000000..3b64971873 --- /dev/null +++ b/v2/pkg/engine/postprocess/schedule_fetches_test.go @@ -0,0 +1,1290 @@ +package postprocess + +import ( + "math" + "math/rand" + "slices" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" +) + +func TestScheduleFetches_Scenarios(t *testing.T) { + t.Parallel() + type scenario struct { + name string + input []*resolve.FetchTreeNode + wantError string + want *resolve.FetchTreeNode // the winner + inlined *resolve.FetchTreeNode // specify when it's not equal to winner + waves *resolve.FetchTreeNode // specify when it's not equal to winner + } + deps := dependsOn + + scenarios := []scenario{ + { + name: "independent components baseline", + input: nodes(sf(0), sf(1), sf(2, deps(0))), + want: par(seq(sf(0), sf(2)), sf(1)), + }, + { + name: "single chain", + input: nodes(sf(0), sf(1, deps(0)), sf(2, deps(1)), sf(3, deps(2))), + want: seq(sf(0), sf(1), sf(2), sf(3)), + }, + { + name: "diamond join", + input: nodes(sf(0), sf(1, deps(0)), sf(2, deps(0)), sf(3, deps(1, 2))), + want: seq(sf(0), par(sf(1), sf(2)), sf(3)), + }, + { + name: "two chains joining", + input: nodes(sf(0), sf(1), sf(2, deps(0)), sf(3, deps(1)), sf(4, deps(2, 3))), + want: seq( + par( + seq(sf(0), sf(2)), + seq(sf(1), sf(3)), + ), + sf(4), + ), + waves: seq( + par(sf(0), sf(1)), + par(sf(2), sf(3)), + sf(4), + ), + }, + { + name: "wide fan out", + input: nodes(sf(0), sf(1, deps(0)), sf(2, deps(0)), sf(3, deps(0)), sf(4, deps(0))), + want: seq(sf(0), par(sf(1), sf(2), sf(3), sf(4))), + }, + { + name: "wide fan in", + input: nodes(sf(0), sf(1), sf(2), sf(3), sf(4, deps(0, 1, 2, 3))), + want: seq(par(sf(0), sf(1), sf(2), sf(3)), sf(4)), + }, + { + name: "independent diamonds", + input: nodes( + sf(0), sf(1, deps(0)), sf(2, deps(0)), sf(3, deps(1, 2)), + sf(4), sf(5, deps(4)), sf(6, deps(4)), sf(7, deps(5, 6)), + ), + want: par( + seq(sf(0), par(sf(1), sf(2)), sf(3)), + seq(sf(4), par(sf(5), sf(6)), sf(7)), + ), + }, + { + // / 1 > 2 \ / 6 > 7 \ + // 0 -> -> 5 -> -> 10 + // \ 3 > 4 / \ 8 > 9 / + name: "sequenced diamonds with chain arms", + input: nodes( + sf(0), sf(1, deps(0)), sf(2, deps(1)), sf(3, deps(0)), sf(4, deps(3)), sf(5, deps(2, 4)), + sf(6, deps(5)), sf(7, deps(6)), sf(8, deps(5)), sf(9, deps(8)), sf(10, deps(7, 9)), + ), + want: seq( + sf(0), + par( + seq(sf(1), sf(2)), + seq(sf(3), sf(4)), + ), + sf(5), + par( + seq(sf(6), sf(7)), + seq(sf(8), sf(9)), + ), + sf(10), + ), + waves: seq( + sf(0), + par(sf(1), sf(3)), + par(sf(2), sf(4)), + sf(5), + par(sf(6), sf(8)), + par(sf(7), sf(9)), + sf(10), + ), + }, + { + name: "sequential chain", + input: nodes(sf(0), sf(1, deps(0)), sf(2, deps(0, 1))), + want: seq(sf(0), sf(1), sf(2)), + }, + { + name: "single fetch", + input: nodes(sf(0)), + want: sf(0), + }, + { + name: "cycle", + input: nodes(sf(0, deps(1)), sf(1, deps(0))), + wantError: "cycle detected in fetch dependency graph", + }, + { + name: "composite key fan in", + input: nodes(sf(0), sf(1, deps(0)), sf(2, deps(0, 1))), + want: seq(sf(0), sf(1), sf(2)), + }, + { + name: "asymmetric chain merge with leaf", + input: nodes(sf(0), sf(1, deps(0)), sf(2, deps(0)), sf(3, deps(1, 2)), sf(4, deps(2))), + want: seq( + sf(0), + par(sf(1), sf(2)), + par(sf(3), sf(4)), + ), + inlined: seq( + sf(0), + par( + sf(1), + seq(sf(2), sf(4)), + ), + sf(3), + ), + }, + { + name: "deep multi parent fan in", + input: nodes(sf(0), sf(1), sf(2), sf(3, deps(0, 1, 2)), sf(4, deps(3)), sf(5, deps(4))), + want: seq(par(sf(0), sf(1), sf(2)), sf(3), sf(4), sf(5)), + }, + { + name: "non inlined n shape", + input: nodes(sf(0), sf(1), sf(2, deps(0)), sf(3, deps(0, 1)), sf(4, deps(1))), + want: seq( + par(sf(0), sf(1)), + par(sf(2), sf(3), sf(4)), + ), + inlined: seq( + par( + seq(sf(0), sf(2)), + seq(sf(1), sf(4)), + ), + sf(3), + ), + }, + { + name: "independent root with shared join", + input: nodes(sf(0), sf(1), sf(2, deps(0, 1)), sf(3)), + want: par( + seq( + par(sf(0), sf(1)), + sf(2)), + sf(3), + ), + }, + { + name: "independent leaf alongside chain", + input: nodes(sf(0), sf(1, deps(0)), sf(2, deps(0, 1)), sf(3, deps(0))), + want: seq( + sf(0), + par( + seq(sf(1), sf(2)), + sf(3), + ), + ), + }, + { + name: "incomparable dominance fallback", + input: nodes(sf(0), sf(1), sf(2, deps(1)), sf(3, deps(0, 2)), sf(4, deps(0, 1))), + want: seq( + par(sf(0), sf(1)), + par( + seq(sf(2), sf(3)), + sf(4), + ), + ), + inlined: seq( + par( + sf(0), + seq(sf(1), sf(2))), + par(sf(3), sf(4)), + ), + }, + { + name: "chain off a shared join with a generation-skipping edge", + input: nodes(sf(0), sf(1), sf(2, deps(0, 1)), sf(3, deps(2)), sf(4, deps(3, 0))), + want: seq( + par(sf(0), sf(1)), + sf(2), + sf(3), + sf(4), + ), + }, + { + name: "weak components with different shapes have the mixed winner strategies", + input: nodes( + sf(0), sf(1), sf(2, deps(0)), sf(3, deps(0, 1)), sf(4, deps(1)), + sf(5), sf(6), sf(7, deps(5)), sf(8, deps(6)), sf(9, deps(7, 8)), + ), + // 2 weak components: the N component (0..4) keeps its waves tree, + // the chains component (5..9) wins with its inlined tree. + // The mixed winner dominates the all-waves tree. + // 2 ->3 ->4 5->7\ + // ^ / ^ / ->9 + // |/ |/ 6->8/ + // 0 1 + want: par( + seq( + par(sf(0), sf(1)), + par(sf(2), sf(3), sf(4)), + ), + seq( + par( + seq(sf(5), sf(7)), + seq(sf(6), sf(8)), + ), + sf(9), + ), + ), + waves: par( + seq( + par(sf(0), sf(1)), + par(sf(2), sf(3), sf(4)), + ), + seq( + par(sf(5), sf(6)), + par(sf(7), sf(8)), + sf(9), + ), + ), + inlined: par( + seq( + par( + seq(sf(0), sf(2)), + seq(sf(1), sf(4)), + ), + sf(3), + ), + seq( + par( + seq(sf(5), sf(7)), + seq(sf(6), sf(8)), + ), + sf(9), + ), + ), + }, + { + name: "wide fan-out with deeply nested user entity chains", + input: nodes( + sf(0), sf(1, deps(0)), sf(3, deps(0)), sf(5, deps(0)), sf(7, deps(0)), sf(9, deps(0)), + sf(10, deps(0)), sf(14, deps(0)), sf(15, deps(14)), sf(16, deps(14)), sf(17, deps(0)), + sf(18, deps(17)), sf(19, deps(17)), sf(32, deps(0)), sf(33, deps(32)), sf(34, deps(32)), + sf(35, deps(0)), sf(36, deps(35)), sf(37, deps(35)), sf(44, deps(0)), sf(45, deps(44)), + sf(46, deps(44)), sf(47, deps(0)), sf(48, deps(47)), sf(49, deps(47)), sf(56, deps(0)), + sf(57, deps(56)), sf(58, deps(56)), sf(59, deps(0)), sf(60, deps(59)), sf(61, deps(59)), + sf(62, deps(0)), sf(63, deps(62)), sf(64, deps(62)), sf(68, deps(0)), sf(69, deps(68)), + sf(82, deps(68)), sf(83, deps(82)), sf(84, deps(82)), sf(85, deps(68)), sf(86, deps(85)), + sf(87, deps(85)), + ), + want: seq( + sf(0), + par( + sf(1), + sf(3), + sf(5), + sf(7), + sf(9), + sf(10), + seq(sf(14), par(sf(15), sf(16))), + seq(sf(17), par(sf(18), sf(19))), + seq(sf(32), par(sf(33), sf(34))), + seq(sf(35), par(sf(36), sf(37))), + seq(sf(44), par(sf(45), sf(46))), + seq(sf(47), par(sf(48), sf(49))), + seq(sf(56), par(sf(57), sf(58))), + seq(sf(59), par(sf(60), sf(61))), + seq(sf(62), par(sf(63), sf(64))), + seq( + sf(68), + par( + sf(69), + seq( + sf(82), + par(sf(83), sf(84))), + seq( + sf(85), + par(sf(86), sf(87))), + ), + ), + ), + ), + // The legacy wave pipeline: + // seq( + // 0, + // par(1, 3, 5, 7, 9, 10, 14, 17, 32, 35, 44, 47, 56, 59, 62, 68), + // par(15, 16, 18, 19, 33, 34, 36, 37, 45, 46, 48, 49, 57, 58, 60, 61, 63, 64, 69, 82, 85), + // par(83, 84, 86, 87), + // ) + // so fetch 83 waits on all 21 second-wave fetches instead of just 0, 68, 69, 82. + }, + { + name: "mixed depth entity chains", + input: nodes( + sf(0), sf(1, deps(0)), sf(3, deps(0)), sf(5, deps(0)), sf(7, deps(0)), sf(9, deps(0)), + sf(10, deps(0)), sf(14, deps(0)), sf(15, deps(14)), sf(16, deps(14)), sf(17, deps(0)), + sf(18, deps(17)), sf(29, deps(0)), sf(30, deps(29)), sf(31, deps(29)), sf(32, deps(0)), + sf(33, deps(32)), sf(39, deps(0)), sf(40, deps(39)), sf(41, deps(39)), sf(42, deps(0)), + sf(43, deps(42)), sf(49, deps(0)), sf(50, deps(49)), sf(51, deps(49)), sf(52, deps(0)), + sf(53, deps(52)), sf(54, deps(0)), sf(55, deps(54)), sf(59, deps(0)), sf(71, deps(59)), + sf(72, deps(71)), sf(73, deps(71)), sf(74, deps(59)), sf(75, deps(74)), + ), + want: seq( + sf(0), + par( + sf(1), + sf(3), + sf(5), + sf(7), + sf(9), + sf(10), + seq(sf(14), + par(sf(15), sf(16))), + seq(sf(17), sf(18)), + seq(sf(29), par(sf(30), sf(31))), + seq(sf(32), sf(33)), + seq(sf(39), par(sf(40), sf(41))), + seq(sf(42), sf(43)), + seq(sf(49), par(sf(50), sf(51))), + seq(sf(52), sf(53)), + seq(sf(54), sf(55)), + seq( + sf(59), + par( + seq( + sf(71), + par(sf(72), sf(73)), + ), + seq(sf(74), sf(75)), + ), + ), + ), + ), + // The legacy wave pipeline: + // seq( + // 0, + // par(1, 3, 5, 7, 9, 10, 14, 17, 29, 32, 39, 42, 49, 52, 54, 59), + // par(15, 16, 18, 30, 31, 33, 40, 41, 43, 50, 51, 53, 55, 71, 74), + // par(72, 73, 75), + // ) + // so fetch 72 waits on all 15 second-wave fetches instead of just 0, 59, 71. + }, + { + name: "inlining wins on exclusive chain beside a late join", + input: nodes( + sf(0), + sf(1, deps(0)), + sf(2, deps(0)), + sf(3, deps(2)), + sf(4, deps(2)), + sf(5, deps(0, 1, 2, 3, 4)), + sf(6, deps(0, 5)), + ), + want: seq( + sf(0), + par( + sf(1), + seq( + sf(2), + par(sf(3), sf(4)), + ), + ), + sf(5), + sf(6), + ), + waves: seq( + sf(0), + par(sf(1), sf(2)), + par(sf(3), sf(4)), + sf(5), + sf(6), + ), + // The legacy wave pipeline: + // seq( + // 0, + // par(1, 2), + // par(3, 4), + // 5, + // 6, + // ) + }, + { + name: "inlining wins on independent chains gathered by ending joins", + input: nodes( + sf(0), + sf(1, deps(0)), sf(2, deps(0)), sf(3, deps(0)), sf(4, deps(0)), + sf(5, deps(4)), sf(6, deps(4)), sf(7, deps(4)), + sf(12, deps(0)), + sf(15, deps(2)), + sf(16, deps(15)), sf(17, deps(15)), + sf(18, deps(0)), + sf(19, deps(18)), sf(20, deps(18)), + sf(21, deps(4)), + sf(24, deps(15)), + sf(25, deps(18)), + sf(26, deps(0, 2, 3, 5, 6, 12, 15, 16, 17, 18, 19, 20, 21, 24, 25)), + sf(27, deps(0, 4, 5, 6, 7, 21)), + sf(28, deps(0, 1, 18, 26, 27)), + ), + want: seq( + sf(0), + par( + sf(1), + seq( + sf(2), + sf(15), + par(sf(16), sf(17), sf(24)), + ), + sf(3), + seq( + sf(4), + par(sf(5), sf(6), sf(7), sf(21)), + sf(27), + ), + sf(12), + seq( + sf(18), + par(sf(19), sf(20), sf(25)), + ), + ), + sf(26), + sf(28), + ), + waves: seq( + sf(0), + par(sf(1), sf(2), sf(3), sf(4), sf(12), sf(18)), + par(sf(5), sf(6), sf(7), sf(15), sf(19), sf(20), sf(21), sf(25)), + par(sf(16), sf(17), sf(24), sf(27)), + sf(26), + sf(28), + ), + // The legacy wave pipeline: + // seq( + // 0, + // par(1, 2, 3, 4, 12, 18), + // par(5, 6, 7, 15, 19, 20, 21, 25), + // par(16, 17, 24, 27), + // 26, + // 28, + // ) + }, + { + // ca50f52a-4345-42ff-aa79-5897f5a001be-4d832881-45b4-443e-9121-5c610bb07b2c-10761258400747712775 + name: "dense shallow joins", + input: nodes( + sf(0), + sf(1, deps(0)), + sf(2, deps(0)), + sf(3, deps(0)), + sf(4, deps(0)), + sf(7, deps(0)), + sf(8, deps(0)), + sf(9, deps(0)), + sf(10, deps(0)), + sf(11, deps(0)), + sf(15, deps(0, 10, 17, 19, 21)), + sf(17, deps(0)), + sf(19, deps(0)), + sf(21, deps(0)), + sf(24, deps(0, 11)), + ), + want: seq( + sf(0), + par( + sf(1), + sf(2), + sf(3), + sf(4), + sf(7), + sf(8), + sf(9), + seq( + par(sf(10), sf(17), sf(19), sf(21)), + sf(15), + ), + seq(sf(11), sf(24)), + ), + ), + // The legacy wave pipeline: + // seq( + // 0, + // par(1, 2, 3, 4, 7, 8, 9, 10, 11, 17, 19, 21), + // par(24, 15), + // ) + }, + { + // 4041a8d2-4558-4c01-88f5-70140491bdc4-e20d3503-ccc8-4a89-b143-e4e79a994d05-12743902971020326245 + name: "dense serial mutation chain", + input: nodes( + sf(0), + sf(1, deps(0)), + sf(2, deps(0, 1)), + sf(3, deps(0, 1, 2)), + sf(4, deps(0, 1, 2, 3)), + sf(5, deps(0, 1, 2, 3, 4)), + sf(6, deps(0, 1, 2, 3, 4, 5)), + sf(7, deps(0, 1, 2, 3, 4, 5, 6)), + sf(8, deps(0, 1, 2, 3, 4, 5, 6, 7)), + sf(9, deps(0, 1, 2, 3, 4, 5, 6, 7, 8)), + sf(10, deps(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)), + sf(11, deps(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)), + sf(12, deps(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)), + sf(13, deps(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)), + sf(14, deps(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13)), + sf(15, deps(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14)), + sf(16, deps(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)), + sf(17, deps(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)), + sf(18, deps(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)), + sf(19, deps(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18)), + sf(20, deps(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19)), + sf(21, deps(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)), + sf(22, deps(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21)), + sf(23, deps(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22)), + ), + want: seq(sf(0), sf(1), sf(2), sf(3), sf(4), sf(5), sf(6), sf(7), sf(8), sf(9), sf(10), sf(11), sf(12), sf(13), sf(14), sf(15), sf(16), sf(17), sf(18), sf(19), sf(20), sf(21), sf(22), sf(23)), + // The legacy wave pipeline: + // seq(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23) + }, + { + // 3becc2e0f56d3d513c374e8035d1533cc6795740193a45c0c07def3f1f11eb0b + name: "deeply nested operation with wide dependencies", + input: deeplyNestedWideDependenciesInput(), + want: seq( + sf(0), + par( + sf(1), + sf(2), + sf(3), + seq( + sf(4), + par(sf(5), sf(50)), + ), + seq( + sf(6), + par( + sf(51), + seq(sf(73), sf(74)), + sf(81), + ), + ), + seq( + par(sf(7), sf(9), sf(35), sf(76), sf(94), sf(99), sf(102), sf(103)), + par( + seq( + par(sf(8), sf(10), sf(11), sf(12), sf(13), sf(14), sf(37), sf(38), sf(53), sf(54), sf(55), + sf(77), sf(82), sf(83), sf(84), sf(85), sf(86), sf(100), sf(110), sf(112), sf(113), sf(128)), + par( + seq(sf(87), sf(117), sf(145)), + seq(sf(105), sf(138)), + sf(141), + sf(146), + seq(sf(147), sf(148), sf(151)), + sf(149), + ), + ), + sf(52), + ), + ), + seq( + sf(18), + par( + sf(59), + seq( + par(sf(60), sf(61)), + sf(106), + ), + ), + ), + seq( + sf(19), + par(sf(20), sf(21), sf(23), sf(24), sf(25), sf(63), sf(64), sf(65), sf(66), sf(67)), + par(sf(107), sf(108)), + sf(139), + sf(150), + sf(152), + ), + seq(sf(32), sf(75)), + sf(33), + sf(41), + seq(sf(42), sf(93), sf(118)), + seq( + sf(43), + par( + seq(sf(95), sf(122)), + sf(101), + ), + ), + seq(sf(44), sf(45), sf(96)), + sf(47), + seq(sf(49), sf(104)), + sf(62), + seq( + sf(78), + par( + seq(sf(114), sf(142)), + seq( + sf(115), + par(sf(143), sf(144)), + ), + ), + ), + seq(sf(97), sf(98)), + ), + ), + inlined: seq( + sf(0), + par( + sf(1), + sf(2), + sf(3), + seq( + sf(4), + par(sf(5), sf(50)), + ), + seq( + sf(6), + par( + sf(51), + seq(sf(73), sf(74)), + sf(81), + ), + ), + seq( + par( + seq( + sf(7), + par(sf(8), sf(52)), + ), + seq( + sf(9), + par(sf(10), sf(11), sf(12), sf(13), sf(14), sf(53), sf(54), sf(55)), + sf(105), + sf(138), + ), + seq( + sf(35), + par( + seq( + par(sf(37), sf(38), sf(83), sf(84), sf(85), sf(86)), + sf(87), + sf(117), + ), + sf(82), + ), + ), + seq( + sf(76), + par(sf(77), sf(110), sf(112), sf(113)), + ), + sf(94), + seq(sf(99), sf(100)), + sf(102), + seq(sf(103), sf(128)), + ), + par( + sf(141), + sf(145), + sf(146), + seq(sf(147), sf(148), sf(151)), + sf(149), + ), + ), + seq( + sf(18), + par( + sf(59), + seq( + par(sf(60), sf(61)), + sf(106), + ), + ), + ), + seq( + sf(19), + par(sf(20), sf(21), sf(23), sf(24), sf(25), sf(63), sf(64), sf(65), sf(66), sf(67)), + par( + seq(sf(107), sf(139)), + sf(108), + ), + sf(150), + sf(152), + ), + seq(sf(32), sf(75)), + sf(33), + sf(41), + seq(sf(42), sf(93), sf(118)), + seq( + sf(43), + par( + seq(sf(95), sf(122)), + sf(101), + ), + ), + seq(sf(44), sf(45), sf(96)), + sf(47), + seq(sf(49), sf(104)), + sf(62), + seq( + sf(78), + par( + seq(sf(114), sf(142)), + seq( + sf(115), + par(sf(143), sf(144)), + ), + ), + ), + seq(sf(97), sf(98)), + ), + ), + // The legacy wave pipeline: + // seq( + // 0, + // par(1, 2, 3, 4, 6, 7, 9, 18, 19, 32, 33, 35, 41, 42, 43, 44, 47, 49, 62, 76, 78, 94, 97, 99, 102, 103), + // par(5, 8, 10, 11, 12, 13, 14, 20, 21, 23, 24, 25, 37, 38, 45, 50, 51, 52, 53, 54, 55, 59, 60, 61, 63, 64, 65, 66, 67, 73, 75, 77, 81, 82, 83, 84, 85, 86, 93, 95, 98, 100, 101, 104, 110, 112, 113, 114, 115, 128), + // par(74, 96, 118, 122, 142, 143, 144, 106, 107, 146, 87, 108, 147, 105, 141, 149), + // par(139, 148, 117, 138), + // par(151, 145, 150), + // 152, + // ) + }, + } + + for i, tc := range scenarios { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + dag, err := newFetchDAG(tc.input) + require.NoError(t, err) + ids := sortedIDs(dag) + + actualInlined, inlinedErr := schedule(ids, dag, true) + actualWaves, wavesErr := schedule(ids, dag, false) + actualWinner, winnerErr := buildScheduleTree(tc.input, dag) + if tc.wantError != "" { + require.EqualError(t, inlinedErr, tc.wantError) + require.EqualError(t, wavesErr, tc.wantError) + require.EqualError(t, winnerErr, tc.wantError) + return + } + + require.NoError(t, inlinedErr) + require.NoError(t, wavesErr) + require.NoError(t, winnerErr) + require.NoError(t, validateSchedule(actualInlined, dag)) + require.NoError(t, validateSchedule(actualWaves, dag)) + require.NoError(t, validateSchedule(actualWinner, dag)) + + byID := fetchesByID(tc.input) + want := materialize(t, tc.want, byID) + expectedInlined := materialize(t, tc.inlined, byID) + expectedWaves := materialize(t, tc.waves, byID) + if expectedInlined == nil { + expectedInlined = want + } + if expectedWaves == nil { + expectedWaves = want + } + requireEqualTrees(t, expectedInlined, actualInlined) + requireEqualTrees(t, expectedWaves, actualWaves) + requireEqualTrees(t, want, actualWinner) + + // Beyond the pinned shapes, the trees must satisfy the scheduler + // invariants under randomized duration profiles. + assertScheduleProperties(t, actualInlined, actualWaves, actualWinner, durationProfiles(tc.input, 50, int64(9000+i))) + }) + } +} + +func TestScheduleFetches_OptionWiring(t *testing.T) { + t.Parallel() + input := func() *resolve.FetchTreeNode { + return seq( + sf(0), + sf(1), + sf(2, dependsOn(0)), + sf(3, dependsOn(1)), + sf(4, dependsOn(2, 3))) + } + wantWaves := seq( + par(sf(0), sf(1)), + par(sf(2, dependsOn(0)), + sf(3, dependsOn(1))), + sf(4, dependsOn(2, 3))) + wantScheduled := seq( + par(seq(sf(0), sf(2, dependsOn(0))), + seq(sf(1), sf(3, dependsOn(1)))), + sf(4, dependsOn(2, 3))) + + // The scheduler is opt-in: the default pipeline organizes legacy waves. + def := input() + NewProcessor().fetchTreeProcessors.organizeFetchTree(def) + requireEqualTrees(t, wantWaves, def) + + scheduled := input() + NewProcessor(EnableScheduleFetches()).fetchTreeProcessors.organizeFetchTree(scheduled) + requireEqualTrees(t, wantScheduled, scheduled) +} + +func TestScheduleFetches_SubscriptionRootStaysSequence(t *testing.T) { + t.Parallel() + // The plan printer renders the Subscription Primary/Rest wrapper only for Sequence roots: + // the scheduled tree must not collapse into the root when it carries a Trigger. + trigger := &resolve.FetchTreeNode{ + Kind: resolve.FetchTreeNodeKindTrigger, + Item: &resolve.FetchItem{Fetch: &resolve.SingleFetch{}}, + } + + single := seq(sf(0)) + single.Trigger = trigger + err := (&scheduleFetches{}).ProcessFetchTree(single) + require.NoError(t, err) + require.Equal(t, resolve.FetchTreeNodeKindSequence, single.Kind) + require.Equal(t, trigger, single.Trigger) + require.Equal(t, nodes(sf(0)), single.ChildNodes) + + parallel := seq(sf(0), sf(1)) + parallel.Trigger = trigger + err = (&scheduleFetches{}).ProcessFetchTree(parallel) + require.NoError(t, err) + require.Equal(t, resolve.FetchTreeNodeKindSequence, parallel.Kind) + require.Equal(t, trigger, parallel.Trigger) + require.Equal(t, nodes(par(sf(0), sf(1))), parallel.ChildNodes) + + // Without a Trigger the root may collapse into the scheduled tree. + sync := seq(sf(0)) + err = (&scheduleFetches{}).ProcessFetchTree(sync) + require.NoError(t, err) + require.Equal(t, sf(0), sync) +} + +func TestScheduleFetches_Validator(t *testing.T) { + t.Run("response-path nesting without a FetchID edge is valid", func(t *testing.T) { + y := sf(0, responsePath("user")) + x := sf(1, responsePath("user.details"), mergePath("user.details")) + tree := par(x, y) + dag, err := newFetchDAG(nodes(x, y)) + require.NoError(t, err) + require.NoError(t, validateSchedule(tree, dag)) + }) + + t.Run("explicit FetchID edge between parallel siblings is invalid", func(t *testing.T) { + y := sf(0) + x := sf(1, dependsOn(0)) + tree := par(x, y) + dag, err := newFetchDAG(nodes(x, y)) + require.NoError(t, err) + require.EqualError(t, validateSchedule(tree, dag), "fetch 1 is scheduled before its dependency 0 completes") + }) + + t.Run("self-dependency is invalid", func(t *testing.T) { + y := sf(0) + x := sf(1, dependsOn(1)) + _, err := newFetchDAG(nodes(x, y)) + require.EqualError(t, err, "self-dependent id 1") + }) + + // Conservation: a schedule must never lose or duplicate a fetch. The + // property tests lean on these rejections for their completeness checks. + t.Run("schedule missing a fetch is invalid", func(t *testing.T) { + input := nodes(sf(0), sf(1, dependsOn(0)), sf(2, dependsOn(0))) + dag, err := newFetchDAG(input) + require.NoError(t, err) + missing := seq(input[0], input[1]) + require.EqualError(t, validateSchedule(missing, dag), "fetch 2 missing from schedule") + }) + + t.Run("schedule duplicating a fetch is invalid", func(t *testing.T) { + input := nodes(sf(0), sf(1, dependsOn(0)), sf(2, dependsOn(0))) + dag, err := newFetchDAG(input) + require.NoError(t, err) + duplicated := seq(input[0], par(input[1], input[2]), input[1]) + require.EqualError(t, validateSchedule(duplicated, dag), "fetch 1 scheduled 2 times") + }) +} + +func TestScheduleFetches_ProcessorFallsBackOnError(t *testing.T) { + t.Parallel() + // on any scheduler/validator error the processor degrades to the LEGACY wave pipeline. + build := func() *resolve.FetchTreeNode { + return seq(sf(7), sf(7, dependsOn(7))) + } + + legacy := build() + (&orderSequenceByDependencies{}).ProcessFetchTree(legacy) + (&createParallelNodes{}).ProcessFetchTree(legacy) + + scheduled := build() + require.NotPanics(t, func() { + NewProcessor(EnableScheduleFetches()).fetchTreeProcessors.organizeFetchTree(scheduled) + }) + require.Equal(t, legacy, scheduled) +} + +// TestScheduleFetches_BigPlan pins the schedule for a realistic 13-fetch plan +func TestScheduleFetches_BigPlan(t *testing.T) { + // Fetch IDs and order preserve the plan-generator's emission order. + input := func() []*resolve.FetchTreeNode { + return nodes( + sf(0), + sf(5), + sf(1, responsePath("users"), dependsOn(0)), + sf(6, responsePath("topProducts"), dependsOn(5)), + sf(11, responsePath("topProducts"), dependsOn(5)), + sf(2, responsePath("users.@.reviews.@.product"), dependsOn(1)), + sf(3, responsePath("users.@.reviews.@.product.reviews.@.author"), dependsOn(1)), + sf(4, responsePath("users.@.reviews.@.product.reviews.@.author.reviews.@.product"), dependsOn(1)), + sf(7, responsePath("topProducts.@.reviews.@.author"), dependsOn(6)), + sf(8, responsePath("topProducts.@.reviews.@.author.reviews.@.product"), dependsOn(6)), + sf(9, responsePath("users.@.reviews.@.product"), dependsOn(1, 2)), + sf(10, responsePath("users.@.reviews.@.product.reviews.@.author.reviews.@.product"), dependsOn(1, 4)), + sf(12, responsePath("topProducts.@.reviews.@.author.reviews.@.product"), dependsOn(6, 8)), + ) + } + + expected := materialize(t, par( + seq( + sf(0), + sf(1), + par( + seq(sf(2), sf(9)), + sf(3), + seq(sf(4), sf(10)), + ), + ), + seq( + sf(5), + par( + seq( + sf(6), + par( + sf(7), + seq(sf(8), sf(12)), + ), + ), + sf(11), + ), + ), + ), fetchesByID(input())) + + t.Run("scheduler produces the tree without errors", func(t *testing.T) { + fetches := input() + dag, err := newFetchDAG(fetches) + require.NoError(t, err) + tree, err := buildScheduleTree(fetches, dag) + require.NoError(t, err) + require.NoError(t, validateSchedule(tree, dag)) + requireEqualTrees(t, expected, tree) + }) + + t.Run("scheduler does not fall back to legacy waves", func(t *testing.T) { + root := seq(input()...) + NewProcessor(EnableScheduleFetches()).fetchTreeProcessors.organizeFetchTree(root) + requireEqualTrees(t, expected, root) + }) +} + +func sortedIDs(d *fetchDAG) []int { + ids := make([]int, 0, len(d.nodes)) + for id := range d.nodes { + ids = append(ids, id) + } + slices.Sort(ids) + return ids +} + +// hasCycle runs Kahn's algorithm over the full DAG. +func hasCycle(d *fetchDAG) bool { + indegree := make(map[int]int, len(d.nodes)) + queue := make([]int, 0, len(d.nodes)) + for id, parents := range d.parents { + indegree[id] = len(parents) + if len(parents) == 0 { + queue = append(queue, id) + } + } + scheduled := 0 + for len(queue) > 0 { + id := queue[0] + queue = queue[1:] + scheduled++ + for child := range d.children[id] { + if indegree[child]--; indegree[child] == 0 { + queue = append(queue, child) + } + } + } + return scheduled != len(d.nodes) +} + +func TestScheduleFetchesPropertiesRandom(t *testing.T) { + t.Parallel() + rng := rand.New(rand.NewSource(42)) + for n := 3; n <= 15; n++ { + for i := range 50 { + input := randomDAG(n, 1.5, rng) + checkScheduleProperties(t, input, durationProfiles(input, 50, int64(n*1000+i))) + } + } +} + +func TestScheduleFetchesPropertiesSmoke(t *testing.T) { + t.Parallel() + rng := rand.New(rand.NewSource(99)) + for _, n := range []int{50, 500} { + input := randomDAG(n, 1.5, rng) + checkScheduleProperties(t, input, durationProfiles(input, 50, int64(n))) + } +} + +func checkScheduleProperties(t *testing.T, input []*resolve.FetchTreeNode, profiles []map[int]int) { + t.Helper() + dag, err := newFetchDAG(input) + require.NoError(t, err) + ids := sortedIDs(dag) + inlined, inlinedErr := schedule(ids, dag, true) + waves, wavesErr := schedule(ids, dag, false) + winner, winnerErr := buildScheduleTree(input, dag) + if len(ids) > 0 && hasCycle(dag) { + require.EqualError(t, inlinedErr, "cycle detected in fetch dependency graph") + require.EqualError(t, wavesErr, "cycle detected in fetch dependency graph") + require.EqualError(t, winnerErr, "cycle detected in fetch dependency graph") + return + } + require.NoErrorf(t, inlinedErr, "input=%v", dependencyList(input)) + require.NoErrorf(t, wavesErr, "input=%v", dependencyList(input)) + require.NoErrorf(t, winnerErr, "input=%v", dependencyList(input)) + // validateSchedule also enforces conservation: every fetch must be scheduled exactly once: + require.NoError(t, validateSchedule(inlined, dag)) + require.NoError(t, validateSchedule(waves, dag)) + require.NoError(t, validateSchedule(winner, dag)) + + assertScheduleProperties(t, inlined, waves, winner, profiles) +} + +// assertScheduleProperties checks the invariants relating the three trees: +// the winner always dominates the waves tree, +// when the inlined tree dominates the waves tree, the winner is exactly the inlined tree. +func assertScheduleProperties(t *testing.T, inlined, waves, winner *resolve.FetchTreeNode, profiles []map[int]int) { + t.Helper() + require.True(t, dominates(winner, waves)) + if dominates(inlined, waves) { + require.Equal(t, inlined, winner) + } + require.LessOrEqual(t, uniformMakespan(winner), uniformMakespan(waves)) + for _, durations := range profiles { + require.LessOrEqual(t, weightedMakespan(winner, durations), weightedMakespan(waves, durations)) + } +} + +func dependencyList(input []*resolve.FetchTreeNode) []resolve.FetchDependencies { + out := make([]resolve.FetchDependencies, 0, len(input)) + for _, node := range input { + out = append(out, *node.Item.Fetch.Dependencies()) + } + return out +} + +func randomDAG(n int, averageDegree float64, rng *rand.Rand) []*resolve.FetchTreeNode { + depLists := make([][]int, n) + p := averageDegree / math.Max(1, float64(n-1)) + for from := range n { + for to := from + 1; to < n; to++ { + if rng.Float64() < p { + depLists[to] = append(depLists[to], from) + } + } + } + out := make([]*resolve.FetchTreeNode, n) + for i := range out { + out[i] = sf(i, dependsOn(depLists[i]...)) + } + return out +} + +func durationProfiles(input []*resolve.FetchTreeNode, count int, seed int64) []map[int]int { + rng := rand.New(rand.NewSource(seed)) + profiles := make([]map[int]int, count) + for i := range profiles { + profile := make(map[int]int, len(input)) + for _, node := range input { + id := node.Item.Fetch.Dependencies().FetchID + profile[id] = max(int(math.Round(math.Exp(rng.Float64()*math.Log(1000)))), 1) + } + profiles[i] = profile + } + return profiles +} + +// uniformMakespan is the tree makespan with every fetch cost 1. +func uniformMakespan(node *resolve.FetchTreeNode) int { + durations := map[int]int{} + for id := range treePredecessors(node) { + durations[id] = 1 + } + return weightedMakespan(node, durations) +} + +// weightedMakespan is the critical-path weight of the tree under durations: +// sequences add, parallels take the maximum. +func weightedMakespan(node *resolve.FetchTreeNode, durations map[int]int) int { + if node == nil { + return 0 + } + switch node.Kind { + case resolve.FetchTreeNodeKindSingle: + return durations[node.Item.Fetch.Dependencies().FetchID] + case resolve.FetchTreeNodeKindSequence: + sum := 0 + for _, child := range node.ChildNodes { + sum += weightedMakespan(child, durations) + } + return sum + case resolve.FetchTreeNodeKindParallel: + maxSpan := 0 + for _, child := range node.ChildNodes { + if m := weightedMakespan(child, durations); m > maxSpan { + maxSpan = m + } + } + return maxSpan + default: + return 0 + } +} + +// deeplyNestedWideDependenciesInput is a real production query. +// Reference: 3becc2e0f56d3d513c374e8035d1533cc6795740193a45c0c07def3f1f11eb0b +func deeplyNestedWideDependenciesInput() []*resolve.FetchTreeNode { + deps := dependsOn + return nodes( + sf(0), + sf(1, deps(0)), + sf(2, deps(0)), + sf(3, deps(0)), + sf(4, deps(0)), + sf(5, deps(4)), + sf(6, deps(0)), + sf(7, deps(0)), + sf(8, deps(7)), + sf(9, deps(0)), + sf(10, deps(9)), + sf(11, deps(9)), + sf(12, deps(9)), + sf(13, deps(9)), + sf(14, deps(9)), + sf(18, deps(0)), + sf(19, deps(0)), + sf(20, deps(19)), + sf(21, deps(19)), + sf(23, deps(19)), + sf(24, deps(19)), + sf(25, deps(19)), + sf(32, deps(0)), + sf(33, deps(0)), + sf(35, deps(0)), + sf(37, deps(35)), + sf(38, deps(35)), + sf(41, deps(0)), + sf(42, deps(0)), + sf(43, deps(0)), + sf(44, deps(0)), + sf(45, deps(44)), + sf(47, deps(0)), + sf(49, deps(0)), + sf(50, deps(4)), + sf(51, deps(6)), + sf(52, deps(7)), + sf(53, deps(9)), + sf(54, deps(9)), + sf(55, deps(9)), + sf(59, deps(18)), + sf(60, deps(18)), + sf(61, deps(18)), + sf(62, deps(0)), + sf(63, deps(19)), + sf(64, deps(19)), + sf(65, deps(19)), + sf(66, deps(19)), + sf(67, deps(19)), + sf(73, deps(6)), + sf(74, deps(6, 73)), + sf(75, deps(32)), + sf(76, deps(0)), + sf(77, deps(76)), + sf(78, deps(0)), + sf(81, deps(6)), + sf(82, deps(35)), + sf(83, deps(35)), + sf(84, deps(35)), + sf(85, deps(35)), + sf(86, deps(35)), + sf(87, deps(35, 37, 38, 83, 84, 85)), + sf(93, deps(42)), + sf(94, deps(0)), + sf(95, deps(43)), + sf(96, deps(44, 45)), + sf(97, deps(0)), + sf(98, deps(0, 97)), + sf(99, deps(0)), + sf(100, deps(99)), + sf(101, deps(43)), + sf(102, deps(0)), + sf(103, deps(0)), + sf(104, deps(49)), + sf(105, deps(9, 11, 12, 13, 14, 54, 55)), + sf(106, deps(18, 60, 61)), + sf(107, deps(19, 21, 64, 65)), + sf(108, deps(19, 20, 25, 63, 66, 67)), + sf(110, deps(76)), + sf(112, deps(76)), + sf(113, deps(76)), + sf(114, deps(78)), + sf(115, deps(78)), + sf(117, deps(35, 84, 85, 86, 87)), + sf(118, deps(93)), + sf(122, deps(95)), + sf(128, deps(103)), + sf(138, deps(9, 10, 11, 12, 53, 105)), + sf(139, deps(19, 21, 63, 64, 107)), + sf(141, deps(0, 76, 77, 86, 110, 112, 113)), + sf(142, deps(0, 78, 114)), + sf(143, deps(0, 78, 115)), + sf(144, deps(0, 78, 115)), + sf(145, deps(0, 8, 35, 37, 82, 117)), + sf(146, deps(0, 8, 14, 94)), + sf(147, deps(0, 86, 99, 100, 110)), + sf(148, deps(147)), + sf(149, deps(0, 13, 14, 54, 55, 102, 103, 110, 128)), + sf(150, deps(19, 20, 23, 24, 25, 63, 108, 139)), + sf(151, deps(147, 148)), + sf(152, deps(19, 150)), + ) +} + +// BenchmarkOrganizeFetchTree runs the dependency-aware scheduler against the legacy wave pipeline. +func BenchmarkOrganizeFetchTree(b *testing.B) { + // Each fetch tree input is ~50 kb / ~400 allocs overhead per iteration below. + b.Run("schedule", func(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + input := deeplyNestedWideDependenciesInput() + root := resolve.Sequence(input...) + err := (&scheduleFetches{}).ProcessFetchTree(root) + if err != nil { + b.Errorf("error: %v", err) + } + } + }) + b.Run("waves", func(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + input := deeplyNestedWideDependenciesInput() + root := resolve.Sequence(input...) + (&orderSequenceByDependencies{}).ProcessFetchTree(root) + (&createParallelNodes{}).ProcessFetchTree(root) + } + }) +} diff --git a/v2/pkg/engine/postprocess/util_test.go b/v2/pkg/engine/postprocess/util_test.go new file mode 100644 index 0000000000..98421bbe86 --- /dev/null +++ b/v2/pkg/engine/postprocess/util_test.go @@ -0,0 +1,162 @@ +package postprocess + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" +) + +// Various test helpers to reduce clutter in tests. + +func nodes(items ...*resolve.FetchTreeNode) []*resolve.FetchTreeNode { + return items +} + +// singleFetchOption configures optional fields of a single-fetch node built by sf. +type singleFetchOption func(node *resolve.FetchTreeNode) + +// dependsOn sets the fetch IDs the fetch depends on. +func dependsOn(ids ...int) singleFetchOption { + return func(node *resolve.FetchTreeNode) { + node.Item.Fetch.(*resolve.SingleFetch).FetchDependencies.DependsOnFetchIDs = ids + } +} + +// responsePath nests the fetch responsePath responsePath, e.g. "a.b" becomes ObjectPath("a"), ObjectPath("b"). +func responsePath(responsePath string) singleFetchOption { + return func(node *resolve.FetchTreeNode) { + segments := strings.Split(responsePath, ".") + path := make([]resolve.FetchItemPathElement, len(segments)) + for i, segment := range segments { + path[i] = resolve.ObjectPath(segment) + } + node.Item.FetchPath = path + node.Item.ResponsePath = responsePath + node.Item.ResponsePathElements = segments + } +} + +// mergePath sets the merge path the fetch mergePath in the response. +func mergePath(mergePath ...string) singleFetchOption { + return func(node *resolve.FetchTreeNode) { + node.Item.Fetch.(*resolve.SingleFetch).PostProcessing.MergePath = mergePath + } +} + +func sf(id int, opts ...singleFetchOption) *resolve.FetchTreeNode { + node := resolve.Single(&resolve.SingleFetch{FetchDependencies: resolve.FetchDependencies{FetchID: id}}) + for _, opt := range opts { + opt(node) + } + return node +} + +func seq(children ...*resolve.FetchTreeNode) *resolve.FetchTreeNode { + return resolve.Sequence(children...) +} + +func par(children ...*resolve.FetchTreeNode) *resolve.FetchTreeNode { + return resolve.Parallel(children...) +} + +// fetchesByID indexes flat input fetch nodes by their FetchID. +func fetchesByID(input []*resolve.FetchTreeNode) map[int]*resolve.FetchTreeNode { + byID := make(map[int]*resolve.FetchTreeNode, len(input)) + for _, node := range input { + byID[node.Item.Fetch.Dependencies().FetchID] = node + } + return byID +} + +// materialize returns shape with every leaf node replaced by the input node carrying the same fetch ID. +// It needed to prevent clutter in the expected part of the test as not important. +func materialize(t *testing.T, shape *resolve.FetchTreeNode, input map[int]*resolve.FetchTreeNode) *resolve.FetchTreeNode { + t.Helper() + if shape == nil { + return nil + } + if shape.Kind == resolve.FetchTreeNodeKindSingle { + id := shape.Item.Fetch.Dependencies().FetchID + node, ok := input[id] + require.Truef(t, ok, "expected tree references fetch %d not present in input", id) + return node + } + children := make([]*resolve.FetchTreeNode, len(shape.ChildNodes)) + for i, child := range shape.ChildNodes { + children[i] = materialize(t, child, input) + } + return &resolve.FetchTreeNode{Kind: shape.Kind, ChildNodes: children} +} + +// requireEqualTrees compares two fetch trees. The rendered tree is valid Go. +func requireEqualTrees(t *testing.T, expected, actual *resolve.FetchTreeNode) { + t.Helper() + require.Equal(t, renderShape(expected), renderShape(actual)) + require.Equal(t, expected, actual) +} + +func renderShape(node *resolve.FetchTreeNode) string { + var b strings.Builder + writeShape(&b, node, 0) + return b.String() +} + +func writeShape(b *strings.Builder, node *resolve.FetchTreeNode, depth int) { + indent := strings.Repeat("\t", depth) + if node == nil { + b.WriteString(indent + "nil") + return + } + switch node.Kind { + case resolve.FetchTreeNodeKindSingle: + b.WriteString(indent + leafShape(node)) + case resolve.FetchTreeNodeKindSequence, resolve.FetchTreeNodeKindParallel: + name := "seq" + if node.Kind == resolve.FetchTreeNodeKindParallel { + name = "par" + } + // A group of only leaves stays on one line. + if leaves := leafShapes(node.ChildNodes); leaves != nil { + b.WriteString(indent + name + "(" + strings.Join(leaves, ", ") + ")") + return + } + b.WriteString(indent + name + "(\n") + for _, child := range node.ChildNodes { + writeShape(b, child, depth+1) + b.WriteString(",\n") + } + b.WriteString(indent + ")") + default: + fmt.Fprintf(b, "%s%s(?)", indent, node.Kind) + } +} + +// leafShapes returns the rendered leaves of children, or nil if any child is not a leaf. +func leafShapes(children []*resolve.FetchTreeNode) []string { + leaves := make([]string, len(children)) + for i, child := range children { + if child == nil || child.Kind != resolve.FetchTreeNodeKindSingle { + return nil + } + leaves[i] = leafShape(child) + } + return leaves +} + +func leafShape(node *resolve.FetchTreeNode) string { + id := node.Item.Fetch.Dependencies().FetchID + switch node.Item.Fetch.(type) { + case *resolve.SingleFetch: + return fmt.Sprintf("sf(%d)", id) + case *resolve.EntityFetch: + return fmt.Sprintf("ef(%d)", id) + case *resolve.BatchEntityFetch: + return fmt.Sprintf("bf(%d)", id) + default: + return fmt.Sprintf("fetch(%d)", id) + } +}