diff --git a/demo/go.mod b/demo/go.mod index 92853f18fe..75d3d9e174 100644 --- a/demo/go.mod +++ b/demo/go.mod @@ -13,9 +13,9 @@ require ( github.com/rs/cors v1.11.0 github.com/vektah/gqlparser/v2 v2.5.16 github.com/wundergraph/cosmo/composition-go v0.0.0-20240124120900-5effe48a4a1d - github.com/wundergraph/cosmo/router v0.0.0-20250107115408-cdd3d47d6424 + github.com/wundergraph/cosmo/router v0.0.0-20250119174948-4b991294658e github.com/wundergraph/cosmo/router-tests v0.0.0-20241213115435-a249dba8c52a - github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.138.0.20250106145350-20e4f82cea6b + github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.141 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 go.opentelemetry.io/otel v1.28.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.23.1 diff --git a/demo/go.sum b/demo/go.sum index 52394df576..530e7d38d6 100644 --- a/demo/go.sum +++ b/demo/go.sum @@ -305,12 +305,12 @@ github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 h1:8/D7f8gKxTB github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083/go.mod h1:eOTL6acwctsN4F3b7YE+eE2t8zcJ/doLm9sZzsxxxrE= github.com/wundergraph/cosmo/composition-go v0.0.0-20240124120900-5effe48a4a1d h1:NEUrhuqOaTO1dpW8pz2tu6dKbQAqFvgiF/m4NXdzZm0= github.com/wundergraph/cosmo/composition-go v0.0.0-20240124120900-5effe48a4a1d/go.mod h1:9I3gPMAlAY+m1/cFL20iN7XHTyuZd3VT5ijccdU/FsI= -github.com/wundergraph/cosmo/router v0.0.0-20250107115408-cdd3d47d6424 h1:X+gTrBNOXyS1b6PjOs7iJspQiBpxqlmX91PpDL5qdgw= -github.com/wundergraph/cosmo/router v0.0.0-20250107115408-cdd3d47d6424/go.mod h1:XR62DDeHO2/vGppFRFlLGB2qLIViiUu1zg9vmYOo87M= +github.com/wundergraph/cosmo/router v0.0.0-20250119174948-4b991294658e h1:ee4fu7klTY98Zsz7kcYiowiK1RBJkwUBLK6KUo250p8= +github.com/wundergraph/cosmo/router v0.0.0-20250119174948-4b991294658e/go.mod h1:ImqCvxvvNOy1UxbuTnFtin/CDBFHoFqrZly3rC2z+e0= github.com/wundergraph/cosmo/router-tests v0.0.0-20241213115435-a249dba8c52a h1:GVLe85f5g+G0IOorDBBNTfm5Ua9DO0vuVY7ReSTOEbQ= github.com/wundergraph/cosmo/router-tests v0.0.0-20241213115435-a249dba8c52a/go.mod h1:I+SFviFnd3BHlPmYn+ckmzQyDB9+/c8RZJo4t6VQAds= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.138.0.20250106145350-20e4f82cea6b h1:DnIV7YVjrPcrJj2awt8M1F++ql6EfC0hB0Or0m4OXx4= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.138.0.20250106145350-20e4f82cea6b/go.mod h1:B7eV0Qh8Lop9QzIOQcsvKp3S0ejfC6mgyWoJnI917yQ= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.141 h1:lPwwEJRYYuJflv7fhgwaWKt6FKRdX5CJ1Yp6RWzzKDA= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.141/go.mod h1:B7eV0Qh8Lop9QzIOQcsvKp3S0ejfC6mgyWoJnI917yQ= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/router-tests/cache_warmup_test.go b/router-tests/cache_warmup_test.go index 07db725185..ce70d79e65 100644 --- a/router-tests/cache_warmup_test.go +++ b/router-tests/cache_warmup_test.go @@ -6,10 +6,11 @@ import ( "time" "github.com/stretchr/testify/require" + "go.uber.org/zap" + "github.com/wundergraph/cosmo/router-tests/testenv" "github.com/wundergraph/cosmo/router/core" "github.com/wundergraph/cosmo/router/pkg/config" - "go.uber.org/zap" ) func TestCacheWarmup(t *testing.T) { @@ -17,6 +18,9 @@ func TestCacheWarmup(t *testing.T) { t.Run("cache warmup tests for filesystem", func(t *testing.T) { t.Parallel() + + const employeeWarmedQueryCount = 1 + t.Run("cache warmup disabled", func(t *testing.T) { t.Parallel() testenv.Run(t, &testenv.Config{}, func(t *testing.T, xEnv *testenv.Environment) { @@ -28,6 +32,9 @@ func TestCacheWarmup(t *testing.T) { }) t.Run("cache warmup enabled", func(t *testing.T) { t.Parallel() + + const employeeQueryCount = 2 + testenv.Run(t, &testenv.Config{ RouterOptions: []core.Option{ core.WithCacheWarmupConfig(&config.CacheWarmupConfiguration{ @@ -41,12 +48,12 @@ func TestCacheWarmup(t *testing.T) { }, AssertCacheMetrics: &testenv.CacheMetricsAssertions{ BaseGraphAssertions: testenv.CacheMetricsAssertion{ - QueryNormalizationMisses: 3, + QueryNormalizationMisses: 3 + employeeWarmedQueryCount + employeeQueryCount, QueryNormalizationHits: 4, - ValidationMisses: 3, - ValidationHits: 4, - PlanMisses: 3, - PlanHits: 4, + ValidationMisses: 3 + employeeWarmedQueryCount, + ValidationHits: 4 + employeeQueryCount, + PlanMisses: 3 + employeeWarmedQueryCount, + PlanHits: 4 + employeeQueryCount, }, }, }, func(t *testing.T, xEnv *testenv.Environment) { @@ -66,6 +73,19 @@ func TestCacheWarmup(t *testing.T) { Query: `query { employees { id details { forename surname } } }`, }) require.Equal(t, `{"data":{"employees":[{"id":1,"details":{"forename":"Jens","surname":"Neuse"}},{"id":2,"details":{"forename":"Dustin","surname":"Deus"}},{"id":3,"details":{"forename":"Stefan","surname":"Avram"}},{"id":4,"details":{"forename":"Björn","surname":"Schwenzer"}},{"id":5,"details":{"forename":"Sergiy","surname":"Petrunin"}},{"id":7,"details":{"forename":"Suvij","surname":"Surya"}},{"id":8,"details":{"forename":"Nithin","surname":"Kumar"}},{"id":10,"details":{"forename":"Eelco","surname":"Wiersma"}},{"id":11,"details":{"forename":"Alexandra","surname":"Neuse"}},{"id":12,"details":{"forename":"David","surname":"Stutt"}}]}}`, res.Body) + + res = xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `query m($id: Int!){ employee(id: $id) { id details { forename surname } } }`, + Variables: []byte(`{"id": 1}`), + }) + require.Equal(t, "HIT", res.Response.Header.Get("x-wg-execution-plan-cache")) + require.Equal(t, `{"data":{"employee":{"id":1,"details":{"forename":"Jens","surname":"Neuse"}}}}`, res.Body) + + res = xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `query { employee(id: 2) { id details { forename surname } } }`, + }) + require.Equal(t, "HIT", res.Response.Header.Get("x-wg-execution-plan-cache")) + require.Equal(t, `{"data":{"employee":{"id":2,"details":{"forename":"Dustin","surname":"Deus"}}}}`, res.Body) }) }) t.Run("cache warmup invalid files", func(t *testing.T) { @@ -337,13 +357,13 @@ func TestCacheWarmup(t *testing.T) { }, AssertCacheMetrics: &testenv.CacheMetricsAssertions{ BaseGraphAssertions: testenv.CacheMetricsAssertion{ - QueryNormalizationMisses: 3, + QueryNormalizationMisses: 3 + employeeWarmedQueryCount, QueryNormalizationHits: 2, - ValidationMisses: 3, + ValidationMisses: 3 + employeeWarmedQueryCount, ValidationHits: 2, - QueryHashMisses: 3, + QueryHashMisses: 3 + employeeWarmedQueryCount, QueryHashHits: 2, - PlanMisses: 3, + PlanMisses: 3 + employeeWarmedQueryCount, PlanHits: 2, }, }, @@ -363,8 +383,8 @@ func TestCacheWarmup(t *testing.T) { t.Run("cache warmup tests for cdn", func(t *testing.T) { t.Parallel() - // keep in sync with testdata/cache_warmup/cdn/operation.json - cdnOperationCount := int64(4) + // keep in sync with testenv/testdata/cache_warmup/cdn/operation.json + cdnOperationCount := int64(5) cdnPOCount := int64(1) featureOperationCount := int64(1) invalidOperationCount := int64(1) @@ -397,6 +417,7 @@ func TestCacheWarmup(t *testing.T) { t.Run("should correctly warm the cache with data from the operation.json file", func(t *testing.T) { t.Parallel() + const employeeQueryCount = 2 testenv.Run(t, &testenv.Config{ RouterOptions: []core.Option{ core.WithCacheWarmupConfig(&config.CacheWarmupConfiguration{ @@ -405,14 +426,16 @@ func TestCacheWarmup(t *testing.T) { }, AssertCacheMetrics: &testenv.CacheMetricsAssertions{ BaseGraphAssertions: testenv.CacheMetricsAssertion{ - QueryNormalizationMisses: cdnOperationCount + featureOperationCount + invalidOperationCount, + // we have additional 2 misses for the employeeQueryCount - because their content differs from what we have in cdn + // this will be possible to solve only by having operation variants populated + QueryNormalizationMisses: cdnOperationCount + featureOperationCount + invalidOperationCount + employeeQueryCount, QueryNormalizationHits: 3, PersistedQueryNormalizationMisses: cdnPOCount, PersistedQueryNormalizationHits: 0, ValidationMisses: cdnOperationCount + cdnPOCount + featureOperationCount + invalidOperationCount, - ValidationHits: 3, + ValidationHits: 3 + employeeQueryCount, PlanMisses: cdnOperationCount + cdnPOCount, - PlanHits: 3, + PlanHits: 3 + employeeQueryCount, }, }, }, func(t *testing.T, xEnv *testenv.Environment) { @@ -428,6 +451,24 @@ func TestCacheWarmup(t *testing.T) { Query: `query { employees { id details { forename surname } } }`, }) require.Equal(t, `{"data":{"employees":[{"id":1,"details":{"forename":"Jens","surname":"Neuse"}},{"id":2,"details":{"forename":"Dustin","surname":"Deus"}},{"id":3,"details":{"forename":"Stefan","surname":"Avram"}},{"id":4,"details":{"forename":"Björn","surname":"Schwenzer"}},{"id":5,"details":{"forename":"Sergiy","surname":"Petrunin"}},{"id":7,"details":{"forename":"Suvij","surname":"Surya"}},{"id":8,"details":{"forename":"Nithin","surname":"Kumar"}},{"id":10,"details":{"forename":"Eelco","surname":"Wiersma"}},{"id":11,"details":{"forename":"Alexandra","surname":"Neuse"}},{"id":12,"details":{"forename":"David","surname":"Stutt"}}]}}`, res.Body) + + // For the next 2 queries below we will: + // - miss normalization cache + // - hit validation cache + // - hit plan cache + + res = xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `query m($id: Int!){ employee(id: $id) { id details { forename surname } } }`, + Variables: []byte(`{"id": 1}`), + }) + require.Equal(t, "HIT", res.Response.Header.Get("x-wg-execution-plan-cache")) + require.Equal(t, `{"data":{"employee":{"id":1,"details":{"forename":"Jens","surname":"Neuse"}}}}`, res.Body) + + res = xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `query { employee(id: 2) { id details { forename surname } } }`, + }) + require.Equal(t, "HIT", res.Response.Header.Get("x-wg-execution-plan-cache")) + require.Equal(t, `{"data":{"employee":{"id":2,"details":{"forename":"Dustin","surname":"Deus"}}}}`, res.Body) }) }) @@ -446,12 +487,12 @@ func TestCacheWarmup(t *testing.T) { BaseGraphAssertions: testenv.CacheMetricsAssertion{ QueryNormalizationMisses: cdnOperationCount + featureOperationCount + invalidOperationCount, QueryNormalizationHits: 0, - PersistedQueryNormalizationMisses: cdnPOCount, + PersistedQueryNormalizationMisses: cdnPOCount + 2, PersistedQueryNormalizationHits: 1, ValidationMisses: cdnOperationCount + cdnPOCount + featureOperationCount + invalidOperationCount, - ValidationHits: 1, + ValidationHits: 2, PlanMisses: cdnOperationCount + cdnPOCount, - PlanHits: 1, + PlanHits: 2, }, }, }, func(t *testing.T, xEnv *testenv.Environment) { @@ -465,6 +506,15 @@ func TestCacheWarmup(t *testing.T) { require.NoError(t, err) require.Equal(t, expected, res.Body) + + res, err = xEnv.MakeGraphQLRequest(testenv.GraphQLRequest{ + OperationName: []byte(`"A"`), + Extensions: []byte(`{"persistedQuery": {"version": 1, "sha256Hash": "e24399f210ef3f16e6e5427a70bb9609ecea7297e99c3e9241d5912d04eabe60"}}`), + Header: header, + }) + require.NoError(t, err) + require.Equal(t, "HIT", res.Response.Header.Get("x-wg-execution-plan-cache")) + require.Equal(t, `{"data":{"employee":{"id":1,"details":{"forename":"Jens","surname":"Neuse"}}}}`, res.Body) }) }) diff --git a/router-tests/go.mod b/router-tests/go.mod index f670a7508e..f82ac3a17b 100644 --- a/router-tests/go.mod +++ b/router-tests/go.mod @@ -24,15 +24,16 @@ require ( github.com/tidwall/gjson v1.18.0 github.com/twmb/franz-go v1.16.1 github.com/twmb/franz-go/pkg/kadm v1.11.0 - github.com/wundergraph/cosmo/demo v0.0.0-20250107115408-cdd3d47d6424 - github.com/wundergraph/cosmo/router v0.0.0-20250107115408-cdd3d47d6424 - github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.139 + github.com/wundergraph/cosmo/demo v0.0.0-20250119174948-4b991294658e + github.com/wundergraph/cosmo/router v0.0.0-20250119174948-4b991294658e + github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.142 go.opentelemetry.io/otel v1.28.0 go.opentelemetry.io/otel/sdk v1.28.0 go.opentelemetry.io/otel/sdk/metric v1.28.0 go.opentelemetry.io/otel/trace v1.28.0 go.uber.org/atomic v1.11.0 go.uber.org/zap v1.27.0 + golang.org/x/net v0.33.0 google.golang.org/protobuf v1.34.2 gopkg.in/yaml.v3 v3.0.1 ) @@ -159,7 +160,6 @@ require ( golang.org/x/crypto v0.31.0 // indirect golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/mod v0.18.0 // indirect - golang.org/x/net v0.33.0 // indirect golang.org/x/sync v0.10.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect diff --git a/router-tests/go.sum b/router-tests/go.sum index d6bd018e95..e101d2bc38 100644 --- a/router-tests/go.sum +++ b/router-tests/go.sum @@ -358,8 +358,8 @@ github.com/vektah/gqlparser/v2 v2.5.16 h1:1gcmLTvs3JLKXckwCwlUagVn/IlV2bwqle0vJ0 github.com/vektah/gqlparser/v2 v2.5.16/go.mod h1:1lz1OeCqgQbQepsGxPVywrjdBHW2T08PUS3pJqepRww= github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 h1:8/D7f8gKxTBjW+SZK4mhxTTBVpxcqeBgWF1Rfmltbfk= github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083/go.mod h1:eOTL6acwctsN4F3b7YE+eE2t8zcJ/doLm9sZzsxxxrE= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.139 h1:ZxdsKeD3igrOpJtpyUk+Y9jC+///mj2MN/t9mDeX/7E= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.139/go.mod h1:B7eV0Qh8Lop9QzIOQcsvKp3S0ejfC6mgyWoJnI917yQ= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.142 h1:CNuk0zoqmoJVP9Wq03GWLvi64Vpq1qwBIdRgV1669U8= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.142/go.mod h1:B7eV0Qh8Lop9QzIOQcsvKp3S0ejfC6mgyWoJnI917yQ= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/router-tests/integration_test.go b/router-tests/integration_test.go index 23b0a15ae8..fbc1e83f4e 100644 --- a/router-tests/integration_test.go +++ b/router-tests/integration_test.go @@ -101,6 +101,8 @@ func TestExecutionPlanCache(t *testing.T) { t.Parallel() testenv.Run(t, &testenv.Config{}, func(t *testing.T, xEnv *testenv.Environment) { + + // MISS - First query res, err := xEnv.MakeGraphQLRequest(testenv.GraphQLRequest{ Query: `query Find($criteria: SearchInput!) {findEmployees(criteria: $criteria){id details {forename surname}}}`, Variables: json.RawMessage(`{"criteria":{"nationality":"GERMAN"}}`), @@ -110,24 +112,28 @@ func TestExecutionPlanCache(t *testing.T) { require.Equal(t, "MISS", res.Response.Header.Get("X-WG-Execution-Plan-Cache")) require.Equal(t, `{"data":{"findEmployees":[{"id":1,"details":{"forename":"Jens","surname":"Neuse"}},{"id":2,"details":{"forename":"Dustin","surname":"Deus"}},{"id":4,"details":{"forename":"Björn","surname":"Schwenzer"}},{"id":11,"details":{"forename":"Alexandra","surname":"Neuse"}}]}}`, res.Body) + // HIT - Same query as above with different variables res, err = xEnv.MakeGraphQLRequest(testenv.GraphQLRequest{ Query: `query Find($criteria: SearchInput!) {findEmployees(criteria: $criteria){id details {forename surname}}}`, - Variables: json.RawMessage(`{"criteria":{"nationality":"GERMAN"}}`), + Variables: json.RawMessage(`{"criteria":{"nationality":"AMERICAN"}}`), }) require.NoError(t, err) require.Equal(t, http.StatusOK, res.Response.StatusCode) require.Equal(t, "HIT", res.Response.Header.Get("X-WG-Execution-Plan-Cache")) - require.Equal(t, `{"data":{"findEmployees":[{"id":1,"details":{"forename":"Jens","surname":"Neuse"}},{"id":2,"details":{"forename":"Dustin","surname":"Deus"}},{"id":4,"details":{"forename":"Björn","surname":"Schwenzer"}},{"id":11,"details":{"forename":"Alexandra","surname":"Neuse"}}]}}`, res.Body) + require.Equal(t, `{"data":{"findEmployees":[{"id":3,"details":{"forename":"Stefan","surname":"Avram"}}]}}`, res.Body) + // HIT - Same query as above with inline variables res, err = xEnv.MakeGraphQLRequest(testenv.GraphQLRequest{ Query: `query Find($criteria: SearchInput! = { nationality: ENGLISH }) {findEmployees(criteria: $criteria){id details {forename surname}}}`, }) require.NoError(t, err) require.Equal(t, http.StatusOK, res.Response.StatusCode) - require.Equal(t, "MISS", res.Response.Header.Get("X-WG-Execution-Plan-Cache")) + require.Equal(t, "HIT", res.Response.Header.Get("X-WG-Execution-Plan-Cache")) require.Equal(t, `{"data":{"findEmployees":[{"id":12,"details":{"forename":"David","surname":"Stutt"}}]}}`, res.Body) + + // HIT - Same query as above but with different whitespace and operation name res, err = xEnv.MakeGraphQLRequest(testenv.GraphQLRequest{ - Query: `query Find($criteria: SearchInput! = { nationality: ENGLISH }) {findEmployees(criteria: $criteria){id details {forename surname}}}`, + Query: `query Foo ($criteria: SearchInput! = { nationality: ENGLISH }) {findEmployees(criteria: $criteria){id details {forename surname}}}`, }) require.NoError(t, err) require.Equal(t, http.StatusOK, res.Response.StatusCode) diff --git a/router-tests/testenv/testdata/cache_warmup/simple/employee.gql b/router-tests/testenv/testdata/cache_warmup/simple/employee.gql new file mode 100644 index 0000000000..c73c0df591 --- /dev/null +++ b/router-tests/testenv/testdata/cache_warmup/simple/employee.gql @@ -0,0 +1 @@ +query { employee(id: 2) { id details { forename surname } } } diff --git a/router-tests/testenv/testdata/cdn/organization/graph/cache_warmup/operations.json b/router-tests/testenv/testdata/cdn/organization/graph/cache_warmup/operations.json index d4313ad4ab..16e6ffc1f5 100644 --- a/router-tests/testenv/testdata/cdn/organization/graph/cache_warmup/operations.json +++ b/router-tests/testenv/testdata/cdn/organization/graph/cache_warmup/operations.json @@ -56,6 +56,12 @@ "request": { "query": "query { employees { id } nonExistentField }" } + }, + { + "hint": "This is normalized representation of an employee query", + "request": { + "query": "query($a: Int!){employee(id: $a){id details {forename surname}}}" + } } ] } \ No newline at end of file diff --git a/router-tests/testenv/testdata/cdn/organization/graph/operations/my-client/e24399f210ef3f16e6e5427a70bb9609ecea7297e99c3e9241d5912d04eabe60.json b/router-tests/testenv/testdata/cdn/organization/graph/operations/my-client/e24399f210ef3f16e6e5427a70bb9609ecea7297e99c3e9241d5912d04eabe60.json new file mode 100644 index 0000000000..ec8991d711 --- /dev/null +++ b/router-tests/testenv/testdata/cdn/organization/graph/operations/my-client/e24399f210ef3f16e6e5427a70bb9609ecea7297e99c3e9241d5912d04eabe60.json @@ -0,0 +1 @@ +{"version":1,"body":"query A {\n employee(id: 1) {\n id\n details {forename surname}\n }\n}\n\n"} \ No newline at end of file diff --git a/router/core/cache_warmup.go b/router/core/cache_warmup.go index 68231d5836..d7621728b2 100644 --- a/router/core/cache_warmup.go +++ b/router/core/cache_warmup.go @@ -3,16 +3,18 @@ package core import ( "context" "errors" - "google.golang.org/protobuf/encoding/protojson" "time" + "go.uber.org/ratelimit" + "go.uber.org/zap" + "google.golang.org/protobuf/encoding/protojson" + "github.com/wundergraph/astjson" - nodev1 "github.com/wundergraph/cosmo/router/gen/proto/wg/cosmo/node/v1" - "github.com/wundergraph/cosmo/router/pkg/config" "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" - "go.uber.org/ratelimit" - "go.uber.org/zap" + + nodev1 "github.com/wundergraph/cosmo/router/gen/proto/wg/cosmo/node/v1" + "github.com/wundergraph/cosmo/router/pkg/config" ) type CacheWarmupItem struct { @@ -276,7 +278,12 @@ func (c *CacheWarmupPlanningProcessor) ProcessOperation(ctx context.Context, ope return err } - _, err = k.Validate(true) + err = k.RemapVariables() + if err != nil { + return err + } + + _, err = k.Validate(true, k.parsedOperation.RemapVariables) if err != nil { return err } diff --git a/router/core/context.go b/router/core/context.go index 9b6c6ba639..716b3dfb82 100644 --- a/router/core/context.go +++ b/router/core/context.go @@ -9,19 +9,18 @@ import ( "time" "github.com/expr-lang/expr/vm" - "github.com/wundergraph/cosmo/router/internal/expr" + "go.opentelemetry.io/otel/attribute" + "go.uber.org/zap" "github.com/wundergraph/astjson" - graphqlmetrics "github.com/wundergraph/cosmo/router/gen/proto/wg/cosmo/graphqlmetrics/v1" - "github.com/wundergraph/cosmo/router/pkg/config" "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/datasource/httpclient" - "go.opentelemetry.io/otel/attribute" + "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" + graphqlmetrics "github.com/wundergraph/cosmo/router/gen/proto/wg/cosmo/graphqlmetrics/v1" + "github.com/wundergraph/cosmo/router/internal/expr" "github.com/wundergraph/cosmo/router/pkg/authentication" + "github.com/wundergraph/cosmo/router/pkg/config" ctrace "github.com/wundergraph/cosmo/router/pkg/trace" - - "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" - "go.uber.org/zap" ) type contextKey int @@ -472,9 +471,11 @@ type operationContext struct { opType OperationType // hash is the hash of the operation with the normalized content and variables. Used for analytics. hash uint64 - // internalHash is the hash of the operation with normalized content. Used for engine / executor caching. + // internalHash is the hash of the operation with the fully normalized content. Used for engine / executor caching. // we can't use the hash for this due to engine limitations in handling variables with the normalized representation internalHash uint64 + // remapVariables is a map of variables that have been remapped to the new names + remapVariables map[string]string // Content is the content of the operation content string variables *astjson.Value diff --git a/router/core/graphql_handler.go b/router/core/graphql_handler.go index 5d344d378c..8197c02f5d 100644 --- a/router/core/graphql_handler.go +++ b/router/core/graphql_handler.go @@ -139,8 +139,9 @@ func (h *GraphQLHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { defer graphqlExecutionSpan.End() ctx := &resolve.Context{ - Variables: requestContext.operation.variables, - Files: requestContext.operation.files, + Variables: requestContext.operation.variables, + RemapVariables: requestContext.operation.remapVariables, + Files: requestContext.operation.files, Request: resolve.Request{ Header: r.Header, }, diff --git a/router/core/graphql_prehandler.go b/router/core/graphql_prehandler.go index 078735d4d6..946adaef37 100644 --- a/router/core/graphql_prehandler.go +++ b/router/core/graphql_prehandler.go @@ -11,26 +11,24 @@ import ( "sync" "time" - "github.com/wundergraph/cosmo/router/internal/expr" - - "github.com/wundergraph/cosmo/router/pkg/config" + "github.com/go-chi/chi/v5/middleware" + "github.com/golang-jwt/jwt/v5" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" otelmetric "go.opentelemetry.io/otel/metric" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" "github.com/wundergraph/astjson" - - "github.com/go-chi/chi/v5/middleware" - "github.com/golang-jwt/jwt/v5" "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/datasource/httpclient" "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/plan" "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" "github.com/wundergraph/graphql-go-tools/v2/pkg/graphqlerrors" - sdktrace "go.opentelemetry.io/otel/sdk/trace" - "go.opentelemetry.io/otel/trace" - "go.uber.org/zap" + "github.com/wundergraph/cosmo/router/internal/expr" "github.com/wundergraph/cosmo/router/pkg/art" + "github.com/wundergraph/cosmo/router/pkg/config" "github.com/wundergraph/cosmo/router/pkg/otel" rtrace "github.com/wundergraph/cosmo/router/pkg/trace" ) @@ -569,7 +567,6 @@ func (h *PreHandler) handleOperation(req *http.Request, variablesParser *astjson engineNormalizeSpan.SetAttributes(otel.WgNormalizationCacheHit.Bool(cached)) requestContext.operation.normalizationCacheHit = operationKit.parsedOperation.NormalizationCacheHit - requestContext.operation.internalHash = operationKit.parsedOperation.InternalID /** * Normalize the variables @@ -590,7 +587,25 @@ func (h *PreHandler) handleOperation(req *http.Request, variablesParser *astjson return err } + err = operationKit.RemapVariables() + if err != nil { + rtrace.AttachErrToSpan(engineNormalizeSpan, err) + + requestContext.operation.normalizationTime = time.Since(startNormalization) + + if !requestContext.operation.traceOptions.ExcludeNormalizeStats { + httpOperation.traceTimings.EndNormalize() + } + + engineNormalizeSpan.End() + + return err + } + requestContext.operation.hash = operationKit.parsedOperation.ID + requestContext.operation.internalHash = operationKit.parsedOperation.InternalID + requestContext.operation.remapVariables = operationKit.parsedOperation.RemapVariables + operationHashString := strconv.FormatUint(operationKit.parsedOperation.ID, 10) operationHashAttribute := otel.WgOperationHash.String(operationHashString) @@ -642,7 +657,7 @@ func (h *PreHandler) handleOperation(req *http.Request, variablesParser *astjson trace.WithSpanKind(trace.SpanKindInternal), trace.WithAttributes(requestContext.telemetry.traceAttrs...), ) - validationCached, err := operationKit.Validate(requestContext.operation.executionOptions.SkipLoader) + validationCached, err := operationKit.Validate(requestContext.operation.executionOptions.SkipLoader, requestContext.operation.remapVariables) if err != nil { rtrace.AttachErrToSpan(engineValidateSpan, err) diff --git a/router/core/operation_planner.go b/router/core/operation_planner.go index 4fc82f4c0f..508c67cda2 100644 --- a/router/core/operation_planner.go +++ b/router/core/operation_planner.go @@ -4,8 +4,6 @@ import ( "errors" "strconv" - graphqlmetricsv1 "github.com/wundergraph/cosmo/router/gen/proto/wg/cosmo/graphqlmetrics/v1" - "github.com/wundergraph/cosmo/router/pkg/graphqlschemausage" "golang.org/x/sync/singleflight" "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" @@ -13,6 +11,9 @@ import ( "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/plan" "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/postprocess" "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" + + graphqlmetricsv1 "github.com/wundergraph/cosmo/router/gen/proto/wg/cosmo/graphqlmetrics/v1" + "github.com/wundergraph/cosmo/router/pkg/graphqlschemausage" ) type planWithMetaData struct { diff --git a/router/core/operation_processor.go b/router/core/operation_processor.go index 1a3da046b0..b33377ccb8 100644 --- a/router/core/operation_processor.go +++ b/router/core/operation_processor.go @@ -21,9 +21,6 @@ import ( "github.com/tidwall/sjson" fastjson "github.com/wundergraph/astjson" - "github.com/wundergraph/cosmo/router/internal/persistedoperation" - "github.com/wundergraph/cosmo/router/internal/unsafebytes" - "github.com/wundergraph/cosmo/router/pkg/config" "github.com/wundergraph/graphql-go-tools/v2/pkg/apollocompatibility" "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" "github.com/wundergraph/graphql-go-tools/v2/pkg/astnormalization" @@ -34,6 +31,10 @@ import ( "github.com/wundergraph/graphql-go-tools/v2/pkg/middleware/operation_complexity" "github.com/wundergraph/graphql-go-tools/v2/pkg/operationreport" "github.com/wundergraph/graphql-go-tools/v2/pkg/variablesvalidation" + + "github.com/wundergraph/cosmo/router/internal/persistedoperation" + "github.com/wundergraph/cosmo/router/internal/unsafebytes" + "github.com/wundergraph/cosmo/router/pkg/config" ) var ( @@ -53,8 +54,9 @@ type ParsedOperation struct { Sha256Hash string // Type is a string representing the operation type. One of // "query", "mutation", "subscription" - Type string - Variables *fastjson.Object + Type string + Variables *fastjson.Object + RemapVariables map[string]string // Files is a list of files, an interface representing the file data needed to be passed forward. Files []httpclient.File // NormalizedRepresentation is the normalized representation of the operation @@ -131,6 +133,7 @@ type parseKit struct { sha256Hash hash.Hash staticNormalizer *astnormalization.OperationNormalizer variablesNormalizer *astnormalization.VariablesNormalizer + variablesRemapper *astnormalization.VariablesMapper printer *astprinter.Printer normalizedOperation *bytes.Buffer variablesValidator *variablesvalidation.VariablesValidator @@ -641,16 +644,6 @@ func (o *OperationKit) normalizePersistedOperation(clientName string, isApq bool } } - // Hash the normalized operation with the static operation name to avoid different IDs for the same operation - err = o.kit.printer.Print(o.kit.doc, o.kit.keyGen) - if err != nil { - return false, errors.WithStack(fmt.Errorf("normalizePersistedOperation failed generating operation hash: %w", err)) - } - - // Generate the operation ID - o.parsedOperation.InternalID = o.kit.keyGen.Sum64() - o.kit.keyGen.Reset() - // Print the operation with the original operation name o.kit.doc.OperationDefinitions[o.operationDefinitionRef].Name = o.originalOperationNameRef err = o.kit.printer.Print(o.kit.doc, o.kit.normalizedOperation) @@ -691,7 +684,6 @@ func (o *OperationKit) normalizeNonPersistedOperation() (cached bool, err error) entry, ok := o.cache.normalizationCache.Get(cacheKey) if ok { o.parsedOperation.NormalizedRepresentation = entry.normalizedRepresentation - o.parsedOperation.InternalID = entry.operationID o.parsedOperation.Type = entry.operationType o.parsedOperation.NormalizationCacheHit = true err = o.setAndParseOperationDoc() @@ -721,9 +713,6 @@ func (o *OperationKit) normalizeNonPersistedOperation() (cached bool, err error) return false, errors.WithStack(fmt.Errorf("normalizeNonPersistedOperation (uncached) failed generating operation hash: %w", err)) } - // Generate the operation ID - o.parsedOperation.InternalID = o.kit.keyGen.Sum64() - // Print the operation with the original operation name o.kit.doc.OperationDefinitions[o.operationDefinitionRef].Name = o.originalOperationNameRef err = o.kit.printer.Print(o.kit.doc, o.kit.normalizedOperation) @@ -822,6 +811,60 @@ func (o *OperationKit) NormalizeVariables() error { return nil } +func (o *OperationKit) RemapVariables() error { + report := &operationreport.Report{} + variablesMap := o.kit.variablesRemapper.NormalizeOperation(o.kit.doc, o.operationProcessor.executor.ClientSchema, report) + if report.HasErrors() { + return &reportError{ + report: report, + } + } + o.parsedOperation.RemapVariables = variablesMap + + // Hash the normalized operation with the static operation name to avoid different IDs for the same operation + err := o.kit.printer.Print(o.kit.doc, o.kit.keyGen) + if err != nil { + return errors.WithStack(fmt.Errorf("RemapVariables failed generating operation hash: %w", err)) + } + + // Print the operation without the operation name to get the pure normalized form + // Afterward we can calculate the operation ID that is used as a stable identifier for analytics + + o.kit.normalizedOperation.Reset() + // store the original name of the operation + nameRef := o.kit.doc.OperationDefinitions[o.operationDefinitionRef].Name + + staticNameRef := o.kit.doc.Input.AppendInputBytes([]byte("")) + o.kit.doc.OperationDefinitions[o.operationDefinitionRef].Name = staticNameRef + + err = o.kit.printer.Print(o.kit.doc, o.kit.normalizedOperation) + if err != nil { + return err + } + // Reset the doc with the original name + o.kit.doc.OperationDefinitions[o.operationDefinitionRef].Name = nameRef + + o.kit.keyGen.Reset() + _, err = o.kit.keyGen.Write(o.kit.normalizedOperation.Bytes()) + if err != nil { + return err + } + + // Generate the operation ID + o.parsedOperation.InternalID = o.kit.keyGen.Sum64() + o.kit.keyGen.Reset() + + o.kit.normalizedOperation.Reset() + err = o.kit.printer.Print(o.kit.doc, o.kit.normalizedOperation) + if err != nil { + return err + } + + o.parsedOperation.NormalizedRepresentation = o.kit.normalizedOperation.String() + + return nil +} + func (o *OperationKit) loadPersistedOperationFromCache(clientName string) (ok bool, includeOpName bool, err error) { if o.cache == nil || o.cache.persistedOperationNormalizationCache == nil { @@ -970,12 +1013,12 @@ func (o *OperationKit) writeSkipIncludeCacheKeyToKeyGen(skipIncludeVariableNames } // Validate validates the operation variables. -func (o *OperationKit) Validate(skipLoader bool) (cacheHit bool, err error) { +func (o *OperationKit) Validate(skipLoader bool, remapVariables map[string]string) (cacheHit bool, err error) { if !skipLoader { // in case we're skipping the loader, it means that we won't execute the operation // this means that we don't need to validate the variables as they are not used // this is useful to return a query plan without having to provide variables - err = o.kit.variablesValidator.Validate(o.kit.doc, o.operationProcessor.executor.ClientSchema, o.kit.doc.Input.Variables) + err = o.kit.variablesValidator.ValidateWithRemap(o.kit.doc, o.operationProcessor.executor.ClientSchema, o.kit.doc.Input.Variables, remapVariables) if err != nil { var invalidVarErr *variablesvalidation.InvalidVariableError if errors.As(err, &invalidVarErr) { @@ -1123,6 +1166,7 @@ func createParseKit(i int, options *parseKitOptions) *parseKit { astnormalization.WithRemoveUnusedVariables(), ), variablesNormalizer: astnormalization.NewVariablesNormalizer(), + variablesRemapper: astnormalization.NewVariablesMapper(), printer: &astprinter.Printer{}, normalizedOperation: &bytes.Buffer{}, variablesValidator: variablesvalidation.NewVariablesValidator(variablesvalidation.VariablesValidatorOptions{ diff --git a/router/core/websocket.go b/router/core/websocket.go index 63e12100f9..f6c1a11ed5 100644 --- a/router/core/websocket.go +++ b/router/core/websocket.go @@ -6,7 +6,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/wundergraph/cosmo/router/internal/expr" "net" "net/http" "regexp" @@ -15,28 +14,29 @@ import ( "syscall" "time" - "github.com/gorilla/websocket" - "github.com/wundergraph/astjson" - rtrace "github.com/wundergraph/cosmo/router/pkg/trace" - "go.opentelemetry.io/otel/attribute" - "github.com/buger/jsonparser" - "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/plan" - "golang.org/x/sync/semaphore" - "github.com/go-chi/chi/v5/middleware" "github.com/gobwas/ws" "github.com/gobwas/ws/wsutil" + "github.com/gorilla/websocket" "github.com/tidwall/gjson" + "go.opentelemetry.io/otel/attribute" + "go.uber.org/atomic" + "go.uber.org/zap" + "golang.org/x/sync/semaphore" + + "github.com/wundergraph/astjson" + "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/plan" + "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" + "github.com/wundergraph/graphql-go-tools/v2/pkg/netpoll" + + "github.com/wundergraph/cosmo/router/internal/expr" "github.com/wundergraph/cosmo/router/internal/wsproto" "github.com/wundergraph/cosmo/router/pkg/authentication" "github.com/wundergraph/cosmo/router/pkg/config" "github.com/wundergraph/cosmo/router/pkg/logging" "github.com/wundergraph/cosmo/router/pkg/statistics" - "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" - "github.com/wundergraph/graphql-go-tools/v2/pkg/netpoll" - "go.uber.org/atomic" - "go.uber.org/zap" + rtrace "github.com/wundergraph/cosmo/router/pkg/trace" ) var ( @@ -815,7 +815,6 @@ func (h *WebSocketConnectionHandler) parseAndPlan(registration *SubscriptionRegi return nil, nil, err } - opContext.internalHash = operationKit.parsedOperation.InternalID opContext.normalizationCacheHit = operationKit.parsedOperation.NormalizationCacheHit if err := operationKit.NormalizeVariables(); err != nil { @@ -823,7 +822,14 @@ func (h *WebSocketConnectionHandler) parseAndPlan(registration *SubscriptionRegi return nil, nil, err } + if err := operationKit.RemapVariables(); err != nil { + opContext.normalizationTime = time.Since(startNormalization) + return nil, nil, err + } + opContext.hash = operationKit.parsedOperation.ID + opContext.internalHash = operationKit.parsedOperation.InternalID + opContext.remapVariables = operationKit.parsedOperation.RemapVariables opContext.normalizationTime = time.Since(startNormalization) opContext.content = operationKit.parsedOperation.NormalizedRepresentation @@ -834,7 +840,7 @@ func (h *WebSocketConnectionHandler) parseAndPlan(registration *SubscriptionRegi startValidation := time.Now() - if _, err := operationKit.Validate(h.plannerOptions.ExecutionOptions.SkipLoader); err != nil { + if _, err := operationKit.Validate(h.plannerOptions.ExecutionOptions.SkipLoader, opContext.remapVariables); err != nil { opContext.validationTime = time.Since(startValidation) return nil, nil, err } @@ -910,6 +916,7 @@ func (h *WebSocketConnectionHandler) executeSubscription(registration *Subscript ID: h.initRequestID, }, RenameTypeNames: h.graphqlHandler.executor.RenameTypeNames, + RemapVariables: operationCtx.remapVariables, TracingOptions: operationCtx.traceOptions, Extensions: operationCtx.extensions, } diff --git a/router/go.mod b/router/go.mod index 36d709fbf8..cdcd4419e1 100644 --- a/router/go.mod +++ b/router/go.mod @@ -34,7 +34,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.139 + github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.142 // Do not upgrade, it renames attributes we rely on go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 go.opentelemetry.io/contrib/propagators/b3 v1.23.0 diff --git a/router/go.sum b/router/go.sum index 77f37d8256..c8e343b554 100644 --- a/router/go.sum +++ b/router/go.sum @@ -274,8 +274,8 @@ github.com/vektah/gqlparser/v2 v2.5.16 h1:1gcmLTvs3JLKXckwCwlUagVn/IlV2bwqle0vJ0 github.com/vektah/gqlparser/v2 v2.5.16/go.mod h1:1lz1OeCqgQbQepsGxPVywrjdBHW2T08PUS3pJqepRww= github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 h1:8/D7f8gKxTBjW+SZK4mhxTTBVpxcqeBgWF1Rfmltbfk= github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083/go.mod h1:eOTL6acwctsN4F3b7YE+eE2t8zcJ/doLm9sZzsxxxrE= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.139 h1:ZxdsKeD3igrOpJtpyUk+Y9jC+///mj2MN/t9mDeX/7E= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.139/go.mod h1:B7eV0Qh8Lop9QzIOQcsvKp3S0ejfC6mgyWoJnI917yQ= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.142 h1:CNuk0zoqmoJVP9Wq03GWLvi64Vpq1qwBIdRgV1669U8= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.142/go.mod h1:B7eV0Qh8Lop9QzIOQcsvKp3S0ejfC6mgyWoJnI917yQ= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=