Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion router-tests/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ require (
github.com/wundergraph/cosmo/demo/pkg/subgraphs/projects v0.0.0-20250715110703-10f2e5f9c79e
github.com/wundergraph/cosmo/router v0.0.0-20260213130455-6e3277e7b850
github.com/wundergraph/cosmo/router-plugin v0.0.0-20250808194725-de123ba1c65e
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.252
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.253
go.opentelemetry.io/otel v1.36.0
go.opentelemetry.io/otel/sdk v1.36.0
go.opentelemetry.io/otel/sdk/metric v1.36.0
Expand Down
4 changes: 2 additions & 2 deletions router-tests/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -356,8 +356,8 @@ github.com/wundergraph/astjson v1.0.0 h1:rETLJuQkMWWW03HCF6WBttEBOu8gi5vznj5KEUP
github.com/wundergraph/astjson v1.0.0/go.mod h1:h12D/dxxnedtLzsKyBLK7/Oe4TAoGpRVC9nDpDrZSWw=
github.com/wundergraph/go-arena v1.1.0 h1:9+wSRkJAkA2vbYHp6s8tEGhPViRGQNGXqPHT0QzhdIc=
github.com/wundergraph/go-arena v1.1.0/go.mod h1:ROOysEHWJjLQ8FSfNxZCziagb7Qw2nXY3/vgKRh7eWw=
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.252 h1:qhm6obHtRwgZm84Gu3G63ywGz2ys2xLBwC0gloDKZuo=
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.252/go.mod h1:MFbY0QI8ncF60DHs7yyyiyyhWyld0WE1JokiyTVY8j4=
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.253 h1:QW7wGThQdplffQHkePTmJ/Lh32ZXB8EEOq3zC/v48yg=
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.253/go.mod h1:MFbY0QI8ncF60DHs7yyyiyyhWyld0WE1JokiyTVY8j4=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
Expand Down
2 changes: 1 addition & 1 deletion router/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ require (
github.com/tidwall/gjson v1.18.0
github.com/tidwall/sjson v1.2.5
github.com/twmb/franz-go v1.16.1
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.252
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.253
// Do not upgrade, it renames attributes we rely on
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0
go.opentelemetry.io/contrib/propagators/b3 v1.23.0
Expand Down
4 changes: 2 additions & 2 deletions router/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -326,8 +326,8 @@ github.com/wundergraph/astjson v1.0.0 h1:rETLJuQkMWWW03HCF6WBttEBOu8gi5vznj5KEUP
github.com/wundergraph/astjson v1.0.0/go.mod h1:h12D/dxxnedtLzsKyBLK7/Oe4TAoGpRVC9nDpDrZSWw=
github.com/wundergraph/go-arena v1.1.0 h1:9+wSRkJAkA2vbYHp6s8tEGhPViRGQNGXqPHT0QzhdIc=
github.com/wundergraph/go-arena v1.1.0/go.mod h1:ROOysEHWJjLQ8FSfNxZCziagb7Qw2nXY3/vgKRh7eWw=
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.252 h1:qhm6obHtRwgZm84Gu3G63ywGz2ys2xLBwC0gloDKZuo=
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.252/go.mod h1:MFbY0QI8ncF60DHs7yyyiyyhWyld0WE1JokiyTVY8j4=
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.253 h1:QW7wGThQdplffQHkePTmJ/Lh32ZXB8EEOq3zC/v48yg=
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.253/go.mod h1:MFbY0QI8ncF60DHs7yyyiyyhWyld0WE1JokiyTVY8j4=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
Expand Down
43 changes: 24 additions & 19 deletions router/pkg/plan_generator/plan_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ type QueryPlanResult struct {
Timings core.OperationTimes `json:"timings,omitempty"`
}

// PlanGenerator reads GraphQL operation files from cfg.SourceDir,
// generates query plans for each using the execution config,
// and writes results to cfg.OutDir as individual files and/or a consolidated report.
func PlanGenerator(ctx context.Context, cfg QueryPlanConfig) error {
if cfg.Concurrency == 0 {
cfg.Concurrency = runtime.GOMAXPROCS(0)
Expand All @@ -60,35 +63,35 @@ func PlanGenerator(ctx context.Context, cfg QueryPlanConfig) error {

queriesPath, err := filepath.Abs(cfg.SourceDir)
if err != nil {
return fmt.Errorf("failed to get absolute path for queries: %v", err)
return fmt.Errorf("failed to get absolute path for queries: %w", err)
}

outPath, err := filepath.Abs(cfg.OutDir)
if err != nil {
return fmt.Errorf("failed to get absolute path for output: %v", err)
return fmt.Errorf("failed to get absolute path for output: %w", err)
}
if err := os.MkdirAll(outPath, 0755); err != nil {
return fmt.Errorf("failed to create output directory: %v", err)
return fmt.Errorf("failed to create output directory: %w", err)
}

executionConfigPath, err := filepath.Abs(cfg.ExecutionConfig)
if err != nil {
return fmt.Errorf("failed to get absolute path for execution config: %v", err)
return fmt.Errorf("failed to get absolute path for execution config: %w", err)
}

var filter []string
if cfg.Filter != "" {
filterContent, err := os.ReadFile(cfg.Filter)
if err != nil {
return fmt.Errorf("failed to read filter file: %v", err)
return fmt.Errorf("failed to read filter file: %w", err)
}

filter = strings.Split(string(filterContent), "\n")
}

queries, err := os.ReadDir(queriesPath)
if err != nil {
return fmt.Errorf("failed to read queries directory: %v", err)
return fmt.Errorf("failed to read queries directory: %w", err)
}

queriesQueue := make(chan os.DirEntry, len(queries))
Expand All @@ -102,7 +105,7 @@ func PlanGenerator(ctx context.Context, cfg QueryPlanConfig) error {

duration, parseErr := time.ParseDuration(cfg.Timeout)
if parseErr != nil {
return fmt.Errorf("failed to parse timeout: %v", parseErr)
return fmt.Errorf("failed to parse timeout: %w", parseErr)
}
ctx, cancel := context.WithTimeout(ctx, duration)
defer cancel()
Expand All @@ -111,7 +114,7 @@ func PlanGenerator(ctx context.Context, cfg QueryPlanConfig) error {

pg, err := core.NewPlanGenerator(executionConfigPath, cfg.Logger, cfg.MaxDataSourceCollectorsConcurrency)
if err != nil {
return fmt.Errorf("failed to create plan generator: %v", err)
return fmt.Errorf("failed to create plan generator: %w", err)
}

var planError atomic.Bool
Expand All @@ -120,13 +123,6 @@ func PlanGenerator(ctx context.Context, cfg QueryPlanConfig) error {
for i := 0; i < cfg.Concurrency; i++ {
go func(i int) {
defer wg.Done()
planner, err := pg.GetPlanner()
if err != nil {
// if we fail to get the planner, we need to cancel the context to stop the other goroutines
// and return here to stop the current goroutine
cancelError(fmt.Errorf("failed to get planner: %v", err))
return
}
for {
select {
case <-ctxError.Done():
Expand All @@ -146,6 +142,15 @@ func PlanGenerator(ctx context.Context, cfg QueryPlanConfig) error {

queryFilePath := filepath.Join(queriesPath, queryFile.Name())

// Planners should not be reused.
planner, err := pg.GetPlanner()
if err != nil {
// If we fail to get the planner, we have to cancel the context
// to stop this and the other goroutines via ctxError.
cancelError(fmt.Errorf("failed to get a planner: %w", err))
return
}

outContent, opTimes, err := planner.PlanOperation(queryFilePath, cfg.OutputFormat)
res := QueryPlanResult{
FileName: queryFile.Name(),
Expand All @@ -170,7 +175,7 @@ func PlanGenerator(ctx context.Context, cfg QueryPlanConfig) error {
outFileName := filepath.Join(outPath, queryFile.Name())
err = os.WriteFile(outFileName, []byte(outContent), 0644)
if err != nil {
cancelError(fmt.Errorf("failed to write file: %v", err))
cancelError(fmt.Errorf("failed to write file: %w", err))
}
}
resultsMux.Lock()
Expand All @@ -187,7 +192,7 @@ func PlanGenerator(ctx context.Context, cfg QueryPlanConfig) error {
reportFile, err := os.Create(reportFilePath)
if err != nil {
cancel()
return fmt.Errorf("failed to create results file: %v", err)
return fmt.Errorf("failed to create results file: %w", err)
}
defer func() {
_ = reportFile.Close()
Expand All @@ -203,11 +208,11 @@ func PlanGenerator(ctx context.Context, cfg QueryPlanConfig) error {
}
data, jsonErr := json.Marshal(resultData)
if jsonErr != nil {
return fmt.Errorf("failed to marshal result: %v", jsonErr)
return fmt.Errorf("failed to marshal result: %w", jsonErr)
}
_, writeErr := fmt.Fprintf(reportFile, "%s\n", data)
if writeErr != nil {
return fmt.Errorf("failed to write result: %v", writeErr)
return fmt.Errorf("failed to write result: %w", writeErr)
}
}

Expand Down
18 changes: 18 additions & 0 deletions router/pkg/plan_generator/plan_generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -407,3 +407,21 @@ func TestPlanGenerator(t *testing.T) {
})

}

func BenchmarkPlanGenerator(b *testing.B) {
tempDir := b.TempDir()
cfg := QueryPlanConfig{
SourceDir: path.Join(getTestDataDir(), "queries", "bench"),
OutDir: tempDir,
ExecutionConfig: path.Join(getTestDataDir(), "execution_config", "base.json"),
Timeout: "30s",
Concurrency: 1,
}
b.ReportAllocs()
for b.Loop() {
err := PlanGenerator(context.Background(), cfg)
if err != nil {
b.Fatal(err)
}
}
}
125 changes: 125 additions & 0 deletions router/pkg/plan_generator/testdata/queries/bench/full.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
query Full {
employees {
# resolved through employees subgraph
id
# overridden by the products subgraph
notes
details {
# resolved through either employees or family subgraph
forename
surname
# resolved through employees subgraph
location {
language
}
# resolved through family subgraph
hasChildren
# maritalStatus can return null
maritalStatus
nationality
# pets can return null
pets {
class
gender
name
... on Cat {
type
}
... on Dog {
breed
}
... on Alligator {
dangerous
}
}
}
# resolved through employees subgraph
role {
departments
title
... on Engineer {
engineerType
}
... on Operator {
operatorType
}
}
# resolved through hobbies subgraph
hobbies {
... on Exercise {
category
}
... on Flying {
planeModels
yearsOfExperience
}
... on Gaming {
genres
name
yearsOfExperience
}
... on Other {
name
}
... on Programming {
languages
}
... on Travelling {
countriesLived {
language
key {
name
}
}
}
}
# resolved through products subgraph
products
}
# can return null
employee(id: 1) {
# resolved through employees subgraph
id
details {
forename
location {
language
}
}
}
teammates(team: OPERATIONS) {
# resolved through employees subgraph
id
...EmployeeNameFragment
# resolved through products subgraph
products
}
productTypes {
... on Documentation {
url(product: SDK)
urls(products: [COSMO, MARKETING])
}
... on Consultancy {
lead {
...EmployeeNameFragment
}
name
}
}
a: findEmployees(criteria: {
hasPets: true, nationality: UKRAINIAN, nested: { maritalStatus: ENGAGED }
}) {
...EmployeeNameFragment
}
b: findEmployees(criteria: {
hasPets: true, nationality: GERMAN, nested: { maritalStatus: MARRIED, hasChildren: true }
}) {
...EmployeeNameFragment
}
}

fragment EmployeeNameFragment on Employee {
details {
forename
}
}
Loading