diff --git a/router-tests/normalization_cache_test.go b/router-tests/normalization_cache_test.go index b669436dfd..efa9fd7680 100644 --- a/router-tests/normalization_cache_test.go +++ b/router-tests/normalization_cache_test.go @@ -1,14 +1,161 @@ package integration import ( + "fmt" "testing" "github.com/stretchr/testify/require" + "github.com/wundergraph/cosmo/router-tests/testenv" "github.com/wundergraph/cosmo/router/core" "github.com/wundergraph/cosmo/router/pkg/config" ) +// cacheHit represents the expected cache hit/miss status for all three normalization stages. +// True value means the cache was hit. +type cacheHit struct { + normalization bool + variables bool + remapping bool +} + +// assertCacheHeaders checks all three normalization cache headers +func assertCacheHeaders(t *testing.T, res *testenv.TestResponse, expected cacheHit) { + t.Helper() + s := func(hit bool) string { + if hit { + return "HIT" + } + return "MISS" + } + + require.Equal(t, s(expected.normalization), res.Response.Header.Get(core.NormalizationCacheHeader), + "Normalization cache hit mismatch") + require.Equal(t, s(expected.variables), res.Response.Header.Get(core.VariablesNormalizationCacheHeader), + "Variables normalization cache hit mismatch") + require.Equal(t, s(expected.remapping), res.Response.Header.Get(core.VariablesRemappingCacheHeader), + "Variables remapping cache hit mismatch") +} + +func TestVarsNormalizationRemappingCaches(t *testing.T) { + t.Parallel() + + t.Run("Basic normalization cache with skip/include", func(t *testing.T) { + t.Parallel() + testenv.Run(t, &testenv.Config{}, func(t *testing.T, xEnv *testenv.Environment) { + f := func(expected cacheHit, skipMouse bool) { + res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + OperationName: []byte(`"Employee"`), + Query: `query Employee( $id: Int! = 4 $withAligators: Boolean! $withCats: Boolean! $skipDogs:Boolean! $skipMouses:Boolean! ) { employee(id: $id) { details { pets { name __typename ...AlligatorFields @include(if: $withAligators) ...CatFields @include(if: $withCats) ...DogFields @skip(if: $skipDogs) ...MouseFields @skip(if: $skipMouses) ...PonyFields @include(if: false) } } } } fragment AlligatorFields on Alligator { __typename class dangerous gender name } fragment CatFields on Cat { __typename class gender name type } fragment DogFields on Dog { __typename breed class gender name } fragment MouseFields on Mouse { __typename class gender name } fragment PonyFields on Pony { __typename class gender name }`, + Variables: []byte(fmt.Sprintf(`{"withAligators": true,"withCats": true,"skipDogs": false,"skipMouses": %t}`, skipMouse)), + }) + assertCacheHeaders(t, res, expected) + require.Equal(t, `{"data":{"employee":{"details":{"pets":[{"name":"Abby","__typename":"Dog","breed":"GOLDEN_RETRIEVER","class":"MAMMAL","gender":"FEMALE"},{"name":"Survivor","__typename":"Pony"}]}}}}`, res.Body) + } + + f(cacheHit{false, false, false}, true) + f(cacheHit{true, true, true}, true) + f(cacheHit{true, true, true}, true) + f(cacheHit{false, false, false}, false) + f(cacheHit{true, true, true}, true) + }) + }) + + t.Run("Variables normalization cache - inline value extraction", func(t *testing.T) { + t.Parallel() + testenv.Run(t, &testenv.Config{}, func(t *testing.T, xEnv *testenv.Environment) { + // Inline value gets extracted to variable + res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `query { employee(id: 1) { id details { forename } } }`, + }) + assertCacheHeaders(t, res, cacheHit{false, false, false}) + + // Same query + res = xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `query { employee(id: 1) { id details { forename } } }`, + }) + assertCacheHeaders(t, res, cacheHit{true, true, true}) + + // Different inline value + res = xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `query { employee(id: 2) { id details { forename } } }`, + }) + assertCacheHeaders(t, res, cacheHit{false, false, true}) + }) + }) + + t.Run("Variables normalization cache - query changes, but variables stay the same", func(t *testing.T) { + t.Parallel() + testenv.Run(t, &testenv.Config{}, func(t *testing.T, xEnv *testenv.Environment) { + res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `query MyQuery($id: Int!) { employee(id: $id) { id } }`, + Variables: []byte(`{"id": 1}`), + }) + require.Equal(t, `{"data":{"employee":{"id":1}}}`, res.Body) + assertCacheHeaders(t, res, cacheHit{false, false, false}) + + // Different query with the same variable value. + res = xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `query MyQuery($id: Int!) { employee(id: $id) { id details { forename }} }`, + Variables: []byte(`{"id": 1}`), + }) + require.Equal(t, `{"data":{"employee":{"id":1,"details":{"forename":"Jens"}}}}`, res.Body) + assertCacheHeaders(t, res, cacheHit{false, false, false}) + }) + }) + + t.Run("Cache key isolation - different operations don't collide", func(t *testing.T) { + t.Parallel() + testenv.Run(t, &testenv.Config{}, func(t *testing.T, xEnv *testenv.Environment) { + // Query A + res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `query A($id: Int!) { employee(id: $id) { id } }`, + Variables: []byte(`{"id": 1}`), + }) + assertCacheHeaders(t, res, cacheHit{false, false, false}) + + // Query B with different structure should miss + res = xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `query B($id: Int!) { employee(id: $id) { id details { forename } } }`, + Variables: []byte(`{"id": 1}`), + }) + assertCacheHeaders(t, res, cacheHit{false, false, false}) + + // Query A again should hit its own cache + res = xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `query A($id: Int!) { employee(id: $id) { id } }`, + Variables: []byte(`{"id": 1}`), + }) + assertCacheHeaders(t, res, cacheHit{true, true, true}) + }) + }) + + t.Run("List coercion with variables normalization cache", func(t *testing.T) { + t.Parallel() + testenv.Run(t, &testenv.Config{}, func(t *testing.T, xEnv *testenv.Environment) { + // Test that list coercion works correctly with caching + res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `query MyQuery($arg: [String!]!) { rootFieldWithListArg(arg: $arg) }`, + Variables: []byte(`{"arg": "single"}`), + }) + require.Equal(t, `{"data":{"rootFieldWithListArg":["single"]}}`, res.Body) + assertCacheHeaders(t, res, cacheHit{false, false, false}) + + // Same structure should hit cache even with different value + res = xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `query MyQuery($arg: [String!]!) { rootFieldWithListArg(arg: $arg) }`, + Variables: []byte(`{"arg": "different"}`), + }) + require.Equal(t, `{"data":{"rootFieldWithListArg":["different"]}}`, res.Body) + // Normalization hits because the query structure is unchanged, + // variables misses because the value differs, + // and remapping hits because the structure remains the same. + assertCacheHeaders(t, res, cacheHit{true, false, true}) + }) + }) + +} + func TestNormalizationCache(t *testing.T) { t.Parallel() diff --git a/router-tests/telemetry/telemetry_test.go b/router-tests/telemetry/telemetry_test.go index 7ebe7cc1fa..a3453b07cc 100644 --- a/router-tests/telemetry/telemetry_test.go +++ b/router-tests/telemetry/telemetry_test.go @@ -578,6 +578,38 @@ func TestFlakyOperationCacheTelemetry(t *testing.T) { )...), Value: 2, }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("type", "hits"), + )...), + Value: 1, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("type", "misses"), + )...), + Value: 2, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("type", "hits"), + )...), + Value: 1, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("type", "misses"), + )...), + Value: 2, + }, { Attributes: attribute.NewSet(append( baseAttributes, @@ -669,6 +701,52 @@ func TestFlakyOperationCacheTelemetry(t *testing.T) { )...), Value: 0, }, + { + Attributes: attribute.NewSet(append(baseAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "added"), + )...), + Value: 2, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "evicted"), + )...), + Value: 0, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "updated"), + )...), + Value: 0, + }, + { + Attributes: attribute.NewSet(append(baseAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "added"), + )...), + Value: 2, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "evicted"), + )...), + Value: 0, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "updated"), + )...), + Value: 0, + }, { Attributes: attribute.NewSet(append(baseAttributes, attribute.String("cache_type", "validation"), @@ -758,6 +836,36 @@ func TestFlakyOperationCacheTelemetry(t *testing.T) { )...), Value: 0, }, + { + Attributes: attribute.NewSet(append(baseAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "added"), + )...), + Value: baseCost * 2, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "evicted"), + )...), + Value: 0, + }, + { + Attributes: attribute.NewSet(append(baseAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "added"), + )...), + Value: baseCost * 2, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "evicted"), + )...), + Value: 0, + }, { Attributes: attribute.NewSet(append(baseAttributes, attribute.String("cache_type", "validation"), @@ -812,6 +920,20 @@ func TestFlakyOperationCacheTelemetry(t *testing.T) { )...), Value: 1024, }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "variables_normalization"), + )...), + Value: 1024, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "remap_variables"), + )...), + Value: 1024, + }, { Attributes: attribute.NewSet(append(baseAttributes, attribute.String("cache_type", "validation"), @@ -956,6 +1078,38 @@ func TestFlakyOperationCacheTelemetry(t *testing.T) { )...), Value: 2, }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("type", "hits"), + )...), + Value: 2, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("type", "misses"), + )...), + Value: 4, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("type", "hits"), + )...), + Value: 2, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("type", "misses"), + )...), + Value: 4, + }, { Attributes: attribute.NewSet(append( baseAttributes, @@ -1047,6 +1201,52 @@ func TestFlakyOperationCacheTelemetry(t *testing.T) { )...), Value: 0, }, + { + Attributes: attribute.NewSet(append(baseAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "added"), + )...), + Value: 4, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "evicted"), + )...), + Value: 0, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "updated"), + )...), + Value: 0, + }, + { + Attributes: attribute.NewSet(append(baseAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "added"), + )...), + Value: 4, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "evicted"), + )...), + Value: 0, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "updated"), + )...), + Value: 0, + }, { Attributes: attribute.NewSet(append(baseAttributes, attribute.String("cache_type", "validation"), @@ -1136,6 +1336,36 @@ func TestFlakyOperationCacheTelemetry(t *testing.T) { )...), Value: 0, }, + { + Attributes: attribute.NewSet(append(baseAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "added"), + )...), + Value: baseCost * 4, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "evicted"), + )...), + Value: 0, + }, + { + Attributes: attribute.NewSet(append(baseAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "added"), + )...), + Value: baseCost * 4, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "evicted"), + )...), + Value: 0, + }, { Attributes: attribute.NewSet(append(baseAttributes, attribute.String("cache_type", "validation"), @@ -1190,6 +1420,20 @@ func TestFlakyOperationCacheTelemetry(t *testing.T) { )...), Value: 1024, }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "variables_normalization"), + )...), + Value: 1024, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "remap_variables"), + )...), + Value: 1024, + }, { Attributes: attribute.NewSet(append(baseAttributes, attribute.String("cache_type", "validation"), @@ -1298,6 +1542,38 @@ func TestFlakyOperationCacheTelemetry(t *testing.T) { )...), Value: 2, }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("type", "hits"), + )...), + Value: 1, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("type", "misses"), + )...), + Value: 2, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("type", "hits"), + )...), + Value: 1, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("type", "misses"), + )...), + Value: 2, + }, { Attributes: attribute.NewSet(append( baseAttributes, @@ -1389,6 +1665,52 @@ func TestFlakyOperationCacheTelemetry(t *testing.T) { )...), Value: 0, }, + { + Attributes: attribute.NewSet(append(baseAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "added"), + )...), + Value: 2, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "evicted"), + )...), + Value: 0, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "updated"), + )...), + Value: 0, + }, + { + Attributes: attribute.NewSet(append(baseAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "added"), + )...), + Value: 2, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "evicted"), + )...), + Value: 0, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "updated"), + )...), + Value: 0, + }, { Attributes: attribute.NewSet(append(baseAttributes, attribute.String("cache_type", "validation"), @@ -1478,6 +1800,36 @@ func TestFlakyOperationCacheTelemetry(t *testing.T) { )...), Value: 0, }, + { + Attributes: attribute.NewSet(append(baseAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "added"), + )...), + Value: baseCost * 2, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "evicted"), + )...), + Value: 0, + }, + { + Attributes: attribute.NewSet(append(baseAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "added"), + )...), + Value: baseCost * 2, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "evicted"), + )...), + Value: 0, + }, { Attributes: attribute.NewSet(append(baseAttributes, attribute.String("cache_type", "validation"), @@ -1532,6 +1884,20 @@ func TestFlakyOperationCacheTelemetry(t *testing.T) { )...), Value: 1024, }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "variables_normalization"), + )...), + Value: 1024, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "remap_variables"), + )...), + Value: 1024, + }, { Attributes: attribute.NewSet(append(baseAttributes, attribute.String("cache_type", "validation"), @@ -1642,6 +2008,38 @@ func TestFlakyOperationCacheTelemetry(t *testing.T) { )...), Value: 2, }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("type", "hits"), + )...), + Value: 1, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("type", "misses"), + )...), + Value: 2, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("type", "hits"), + )...), + Value: 1, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("type", "misses"), + )...), + Value: 2, + }, { Attributes: attribute.NewSet(append( baseAttributes, @@ -1733,6 +2131,52 @@ func TestFlakyOperationCacheTelemetry(t *testing.T) { )...), Value: 0, }, + { + Attributes: attribute.NewSet(append(baseAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "added"), + )...), + Value: 2, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "evicted"), + )...), + Value: 0, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "updated"), + )...), + Value: 0, + }, + { + Attributes: attribute.NewSet(append(baseAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "added"), + )...), + Value: 2, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "evicted"), + )...), + Value: 0, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "updated"), + )...), + Value: 0, + }, { Attributes: attribute.NewSet(append(baseAttributes, attribute.String("cache_type", "validation"), @@ -1822,6 +2266,36 @@ func TestFlakyOperationCacheTelemetry(t *testing.T) { )...), Value: 0, }, + { + Attributes: attribute.NewSet(append(baseAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "added"), + )...), + Value: baseCost * 2, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "evicted"), + )...), + Value: 0, + }, + { + Attributes: attribute.NewSet(append(baseAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "added"), + )...), + Value: baseCost * 2, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "evicted"), + )...), + Value: 0, + }, { Attributes: attribute.NewSet(append(baseAttributes, attribute.String("cache_type", "validation"), @@ -1876,6 +2350,20 @@ func TestFlakyOperationCacheTelemetry(t *testing.T) { )...), Value: 1024, }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "variables_normalization"), + )...), + Value: 1024, + }, + { + Attributes: attribute.NewSet(append( + baseAttributes, + attribute.String("cache_type", "remap_variables"), + )...), + Value: 1024, + }, { Attributes: attribute.NewSet(append(baseAttributes, attribute.String("cache_type", "validation"), @@ -1987,14 +2475,46 @@ func TestFlakyOperationCacheTelemetry(t *testing.T) { { Attributes: attribute.NewSet(append( mainAttributes, - attribute.String("cache_type", "plan"), + attribute.String("cache_type", "plan"), + attribute.String("type", "hits"), + )...), + Value: 1, + }, + { + Attributes: attribute.NewSet(append(mainAttributes, + attribute.String("cache_type", "plan"), + attribute.String("type", "misses"), + )...), + Value: 2, + }, + { + Attributes: attribute.NewSet(append( + mainAttributes, + attribute.String("cache_type", "query_normalization"), + attribute.String("type", "hits"), + )...), + Value: 1, + }, + { + Attributes: attribute.NewSet(append( + mainAttributes, + attribute.String("cache_type", "query_normalization"), + attribute.String("type", "misses"), + )...), + Value: 2, + }, + { + Attributes: attribute.NewSet(append( + mainAttributes, + attribute.String("cache_type", "variables_normalization"), attribute.String("type", "hits"), )...), Value: 1, }, { - Attributes: attribute.NewSet(append(mainAttributes, - attribute.String("cache_type", "plan"), + Attributes: attribute.NewSet(append( + mainAttributes, + attribute.String("cache_type", "variables_normalization"), attribute.String("type", "misses"), )...), Value: 2, @@ -2002,7 +2522,7 @@ func TestFlakyOperationCacheTelemetry(t *testing.T) { { Attributes: attribute.NewSet(append( mainAttributes, - attribute.String("cache_type", "query_normalization"), + attribute.String("cache_type", "remap_variables"), attribute.String("type", "hits"), )...), Value: 1, @@ -2010,7 +2530,7 @@ func TestFlakyOperationCacheTelemetry(t *testing.T) { { Attributes: attribute.NewSet(append( mainAttributes, - attribute.String("cache_type", "query_normalization"), + attribute.String("cache_type", "remap_variables"), attribute.String("type", "misses"), )...), Value: 2, @@ -2080,6 +2600,38 @@ func TestFlakyOperationCacheTelemetry(t *testing.T) { )...), Value: 2, }, + { + Attributes: attribute.NewSet(append( + featureFlagAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("type", "hits"), + )...), + Value: 1, + }, + { + Attributes: attribute.NewSet(append( + featureFlagAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("type", "misses"), + )...), + Value: 2, + }, + { + Attributes: attribute.NewSet(append( + featureFlagAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("type", "hits"), + )...), + Value: 1, + }, + { + Attributes: attribute.NewSet(append( + featureFlagAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("type", "misses"), + )...), + Value: 2, + }, { Attributes: attribute.NewSet(append( featureFlagAttributes, @@ -2173,6 +2725,54 @@ func TestFlakyOperationCacheTelemetry(t *testing.T) { )...), Value: 0, }, + { + Attributes: attribute.NewSet(append( + mainAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "added"), + )...), + Value: 2, + }, + { + Attributes: attribute.NewSet(append( + mainAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "evicted"), + )...), + Value: 0, + }, + { + Attributes: attribute.NewSet(append( + mainAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "updated"), + )...), + Value: 0, + }, + { + Attributes: attribute.NewSet(append( + mainAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "added"), + )...), + Value: 2, + }, + { + Attributes: attribute.NewSet(append( + mainAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "evicted"), + )...), + Value: 0, + }, + { + Attributes: attribute.NewSet(append( + mainAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "updated"), + )...), + Value: 0, + }, { Attributes: attribute.NewSet(append( mainAttributes, @@ -2271,6 +2871,54 @@ func TestFlakyOperationCacheTelemetry(t *testing.T) { )...), Value: 0, }, + { + Attributes: attribute.NewSet(append( + featureFlagAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "added"), + )...), + Value: 2, + }, + { + Attributes: attribute.NewSet(append( + featureFlagAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "evicted"), + )...), + Value: 0, + }, + { + Attributes: attribute.NewSet(append( + featureFlagAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "updated"), + )...), + Value: 0, + }, + { + Attributes: attribute.NewSet(append( + featureFlagAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "added"), + )...), + Value: 2, + }, + { + Attributes: attribute.NewSet(append( + featureFlagAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "evicted"), + )...), + Value: 0, + }, + { + Attributes: attribute.NewSet(append( + featureFlagAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "updated"), + )...), + Value: 0, + }, { Attributes: attribute.NewSet(append( featureFlagAttributes, @@ -2364,6 +3012,38 @@ func TestFlakyOperationCacheTelemetry(t *testing.T) { )...), Value: 0, }, + { + Attributes: attribute.NewSet(append( + mainAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "added"), + )...), + Value: baseCost * 2, + }, + { + Attributes: attribute.NewSet(append( + mainAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "evicted"), + )...), + Value: 0, + }, + { + Attributes: attribute.NewSet(append( + mainAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "added"), + )...), + Value: baseCost * 2, + }, + { + Attributes: attribute.NewSet(append( + mainAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "evicted"), + )...), + Value: 0, + }, { Attributes: attribute.NewSet(append( mainAttributes, @@ -2429,6 +3109,38 @@ func TestFlakyOperationCacheTelemetry(t *testing.T) { )...), Value: 0, }, + { + Attributes: attribute.NewSet(append( + featureFlagAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "added"), + )...), + Value: baseCost * 2, + }, + { + Attributes: attribute.NewSet(append( + featureFlagAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("operation", "evicted"), + )...), + Value: 0, + }, + { + Attributes: attribute.NewSet(append( + featureFlagAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "added"), + )...), + Value: baseCost * 2, + }, + { + Attributes: attribute.NewSet(append( + featureFlagAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("operation", "evicted"), + )...), + Value: 0, + }, { Attributes: attribute.NewSet(append( featureFlagAttributes, @@ -2486,6 +3198,20 @@ func TestFlakyOperationCacheTelemetry(t *testing.T) { )...), Value: 1024, }, + { + Attributes: attribute.NewSet(append( + mainAttributes, + attribute.String("cache_type", "variables_normalization"), + )...), + Value: 1024, + }, + { + Attributes: attribute.NewSet(append( + mainAttributes, + attribute.String("cache_type", "remap_variables"), + )...), + Value: 1024, + }, { Attributes: attribute.NewSet(append( mainAttributes, @@ -2515,6 +3241,20 @@ func TestFlakyOperationCacheTelemetry(t *testing.T) { )...), Value: 1024, }, + { + Attributes: attribute.NewSet(append( + featureFlagAttributes, + attribute.String("cache_type", "variables_normalization"), + )...), + Value: 1024, + }, + { + Attributes: attribute.NewSet(append( + featureFlagAttributes, + attribute.String("cache_type", "remap_variables"), + )...), + Value: 1024, + }, { Attributes: attribute.NewSet(append( featureFlagAttributes, @@ -3015,7 +3755,7 @@ func TestFlakyTelemetry(t *testing.T) { // Span attributes - require.Len(t, sn[2].Attributes(), 11) + require.Len(t, sn[2].Attributes(), 13) require.Contains(t, sn[2].Attributes(), otel.WgRouterVersion.String("dev")) require.Contains(t, sn[2].Attributes(), otel.WgRouterClusterName.String("")) @@ -3737,7 +4477,7 @@ func TestFlakyTelemetry(t *testing.T) { // Span attributes - require.Len(t, sn[2].Attributes(), 11) + require.Len(t, sn[2].Attributes(), 13) require.Contains(t, sn[2].Attributes(), otel.WgRouterVersion.String("dev")) require.Contains(t, sn[2].Attributes(), otel.WgRouterClusterName.String("")) @@ -4192,7 +4932,7 @@ func TestFlakyTelemetry(t *testing.T) { // Span attributes - require.Len(t, sn[2].Attributes(), 11) + require.Len(t, sn[2].Attributes(), 13) require.Contains(t, sn[2].Attributes(), otel.WgRouterVersion.String("dev")) require.Contains(t, sn[2].Attributes(), otel.WgRouterClusterName.String("")) @@ -6354,7 +7094,7 @@ func TestFlakyTelemetry(t *testing.T) { require.Contains(t, sn[1].Attributes(), otel.WgFeatureFlag.String("myff")) require.Equal(t, "Operation - Normalize", sn[2].Name()) - require.Len(t, sn[2].Attributes(), 12) + require.Len(t, sn[2].Attributes(), 14) require.Contains(t, sn[2].Attributes(), otel.WgRouterConfigVersion.String(xEnv.RouterConfigVersionMyFF())) require.Contains(t, sn[2].Attributes(), otel.WgFeatureFlag.String("myff")) @@ -8811,7 +9551,7 @@ func TestFlakyTelemetry(t *testing.T) { require.Equal(t, "Operation - Normalize", sn[2].Name()) require.Len(t, sn[2].Resource().Attributes(), 9) - require.Len(t, sn[2].Attributes(), 11) + require.Len(t, sn[2].Attributes(), 13) require.Equal(t, "Operation - Validate", sn[3].Name()) require.Len(t, sn[3].Resource().Attributes(), 9) @@ -10961,6 +11701,34 @@ func TestExcludeAttributesWithCustomExporter(t *testing.T) { attribute.String("type", "misses"), )...), }, + { + Attributes: attribute.NewSet(append( + mainAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("type", "hits"), + )...), + }, + { + Attributes: attribute.NewSet(append( + mainAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("type", "misses"), + )...), + }, + { + Attributes: attribute.NewSet(append( + mainAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("type", "hits"), + )...), + }, + { + Attributes: attribute.NewSet(append( + mainAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("type", "misses"), + )...), + }, { Attributes: attribute.NewSet(append( mainAttributes, @@ -11018,6 +11786,34 @@ func TestExcludeAttributesWithCustomExporter(t *testing.T) { attribute.String("type", "misses"), )...), }, + { + Attributes: attribute.NewSet(append( + featureFlagAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("type", "hits"), + )...), + }, + { + Attributes: attribute.NewSet(append( + featureFlagAttributes, + attribute.String("cache_type", "variables_normalization"), + attribute.String("type", "misses"), + )...), + }, + { + Attributes: attribute.NewSet(append( + featureFlagAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("type", "hits"), + )...), + }, + { + Attributes: attribute.NewSet(append( + featureFlagAttributes, + attribute.String("cache_type", "remap_variables"), + attribute.String("type", "misses"), + )...), + }, { Attributes: attribute.NewSet(append( featureFlagAttributes, diff --git a/router/core/cache_warmup.go b/router/core/cache_warmup.go index 2689d7c5d4..b914567291 100644 --- a/router/core/cache_warmup.go +++ b/router/core/cache_warmup.go @@ -295,12 +295,12 @@ func (c *CacheWarmupPlanningProcessor) ProcessOperation(ctx context.Context, ope return nil, err } - _, err = k.NormalizeVariables() + _, _, err = k.NormalizeVariables() if err != nil { return nil, err } - err = k.RemapVariables(c.disableVariablesRemapping) + _, err = k.RemapVariables(c.disableVariablesRemapping) if err != nil { return nil, err } diff --git a/router/core/context.go b/router/core/context.go index 4cbde8706a..5dccb19915 100644 --- a/router/core/context.go +++ b/router/core/context.go @@ -540,8 +540,10 @@ type operationContext struct { sha256Hash string protocol OperationProtocol - persistedOperationCacheHit bool - normalizationCacheHit bool + persistedOperationCacheHit bool + normalizationCacheHit bool + variablesNormalizationCacheHit bool + variablesRemappingCacheHit bool typeFieldUsageInfo graphqlschemausage.TypeFieldMetrics argumentUsageInfo []*graphqlmetrics.ArgumentUsageInfo diff --git a/router/core/graph_server.go b/router/core/graph_server.go index 823a7629c3..348a0d00de 100644 --- a/router/core/graph_server.go +++ b/router/core/graph_server.go @@ -506,18 +506,22 @@ func (s *graphServer) setupEngineStatistics(baseAttributes []attribute.KeyValue) } type graphMux struct { - mux *chi.Mux - planCache *ristretto.Cache[uint64, *planWithMetaData] - persistedOperationCache *ristretto.Cache[uint64, NormalizationCacheEntry] - normalizationCache *ristretto.Cache[uint64, NormalizationCacheEntry] - complexityCalculationCache *ristretto.Cache[uint64, ComplexityCacheEntry] - validationCache *ristretto.Cache[uint64, bool] - operationHashCache *ristretto.Cache[uint64, string] - accessLogsFileLogger *logging.BufferedLogger - metricStore rmetric.Store - prometheusCacheMetrics *rmetric.CacheMetrics - otelCacheMetrics *rmetric.CacheMetrics - streamMetricStore rmetric.StreamMetricStore + mux *chi.Mux + + planCache *ristretto.Cache[uint64, *planWithMetaData] + persistedOperationCache *ristretto.Cache[uint64, NormalizationCacheEntry] + normalizationCache *ristretto.Cache[uint64, NormalizationCacheEntry] + complexityCalculationCache *ristretto.Cache[uint64, ComplexityCacheEntry] + variablesNormalizationCache *ristretto.Cache[uint64, VariablesNormalizationCacheEntry] + remapVariablesCache *ristretto.Cache[uint64, RemapVariablesCacheEntry] + validationCache *ristretto.Cache[uint64, bool] + operationHashCache *ristretto.Cache[uint64, string] + + accessLogsFileLogger *logging.BufferedLogger + metricStore rmetric.Store + prometheusCacheMetrics *rmetric.CacheMetrics + otelCacheMetrics *rmetric.CacheMetrics + streamMetricStore rmetric.StreamMetricStore } // buildOperationCaches creates the caches for the graph mux. @@ -572,6 +576,30 @@ func (s *graphMux) buildOperationCaches(srv *graphServer) (computeSha256 bool, e if err != nil { return computeSha256, fmt.Errorf("failed to create normalization cache: %w", err) } + + variablesNormalizationCacheConfig := &ristretto.Config[uint64, VariablesNormalizationCacheEntry]{ + Metrics: srv.metricConfig.OpenTelemetry.GraphqlCache || srv.metricConfig.Prometheus.GraphqlCache, + MaxCost: srv.engineExecutionConfiguration.NormalizationCacheSize, + NumCounters: srv.engineExecutionConfiguration.NormalizationCacheSize * 10, + IgnoreInternalCost: true, + BufferItems: 64, + } + s.variablesNormalizationCache, err = ristretto.NewCache[uint64, VariablesNormalizationCacheEntry](variablesNormalizationCacheConfig) + if err != nil { + return computeSha256, fmt.Errorf("failed to create normalization cache: %w", err) + } + + remapVariablesCacheConfig := &ristretto.Config[uint64, RemapVariablesCacheEntry]{ + Metrics: srv.metricConfig.OpenTelemetry.GraphqlCache || srv.metricConfig.Prometheus.GraphqlCache, + MaxCost: srv.engineExecutionConfiguration.NormalizationCacheSize, + NumCounters: srv.engineExecutionConfiguration.NormalizationCacheSize * 10, + IgnoreInternalCost: true, + BufferItems: 64, + } + s.remapVariablesCache, err = ristretto.NewCache[uint64, RemapVariablesCacheEntry](remapVariablesCacheConfig) + if err != nil { + return computeSha256, fmt.Errorf("failed to create normalization cache: %w", err) + } } if srv.engineExecutionConfiguration.EnableValidationCache && srv.engineExecutionConfiguration.ValidationCacheSize > 0 { @@ -681,6 +709,14 @@ func (s *graphMux) configureCacheMetrics(srv *graphServer, baseOtelAttributes [] metricInfos = append(metricInfos, rmetric.NewCacheMetricInfo("query_normalization", srv.engineExecutionConfiguration.NormalizationCacheSize, s.normalizationCache.Metrics)) } + if s.variablesNormalizationCache != nil { + metricInfos = append(metricInfos, rmetric.NewCacheMetricInfo("variables_normalization", srv.engineExecutionConfiguration.NormalizationCacheSize, s.variablesNormalizationCache.Metrics)) + } + + if s.remapVariablesCache != nil { + metricInfos = append(metricInfos, rmetric.NewCacheMetricInfo("remap_variables", srv.engineExecutionConfiguration.NormalizationCacheSize, s.remapVariablesCache.Metrics)) + } + if s.persistedOperationCache != nil { metricInfos = append(metricInfos, rmetric.NewCacheMetricInfo("persisted_query_normalization", 1024, s.persistedOperationCache.Metrics)) } @@ -709,31 +745,16 @@ func (s *graphMux) configureCacheMetrics(srv *graphServer, baseOtelAttributes [] } func (s *graphMux) Shutdown(ctx context.Context) error { - var err error + s.planCache.Close() + s.persistedOperationCache.Close() + s.normalizationCache.Close() + s.variablesNormalizationCache.Close() + s.remapVariablesCache.Close() + s.complexityCalculationCache.Close() + s.validationCache.Close() + s.operationHashCache.Close() - if s.planCache != nil { - s.planCache.Close() - } - - if s.persistedOperationCache != nil { - s.persistedOperationCache.Close() - } - - if s.normalizationCache != nil { - s.normalizationCache.Close() - } - - if s.complexityCalculationCache != nil { - s.complexityCalculationCache.Close() - } - - if s.validationCache != nil { - s.validationCache.Close() - } - - if s.operationHashCache != nil { - s.operationHashCache.Close() - } + var err error if s.accessLogsFileLogger != nil { if aErr := s.accessLogsFileLogger.Close(); aErr != nil { @@ -1224,6 +1245,8 @@ func (s *graphServer) buildGraphMux( EnablePersistedOperationsCache: s.engineExecutionConfiguration.EnablePersistedOperationsCache, PersistedOpsNormalizationCache: gm.persistedOperationCache, NormalizationCache: gm.normalizationCache, + VariablesNormalizationCache: gm.variablesNormalizationCache, + RemapVariablesCache: gm.remapVariablesCache, ValidationCache: gm.validationCache, QueryDepthCache: gm.complexityCalculationCache, OperationHashCache: gm.operationHashCache, diff --git a/router/core/graphql_handler.go b/router/core/graphql_handler.go index c494fff4ce..873f2264a4 100644 --- a/router/core/graphql_handler.go +++ b/router/core/graphql_handler.go @@ -34,9 +34,11 @@ var ( ) const ( - ExecutionPlanCacheHeader = "X-WG-Execution-Plan-Cache" - PersistedOperationCacheHeader = "X-WG-Persisted-Operation-Cache" - NormalizationCacheHeader = "X-WG-Normalization-Cache" + ExecutionPlanCacheHeader = "X-WG-Execution-Plan-Cache" + PersistedOperationCacheHeader = "X-WG-Persisted-Operation-Cache" + NormalizationCacheHeader = "X-WG-Normalization-Cache" + VariablesNormalizationCacheHeader = "X-WG-Variables-Normalization-Cache" + VariablesRemappingCacheHeader = "X-WG-Variables-Remapping-Cache" ) type ReportError interface { @@ -428,25 +430,22 @@ func (h *GraphQLHandler) WriteError(ctx *resolve.Context, err error, res *resolv } func (h *GraphQLHandler) setDebugCacheHeaders(w http.ResponseWriter, opCtx *operationContext) { - if h.enableNormalizationCacheResponseHeader { - if opCtx.normalizationCacheHit { - w.Header().Set(NormalizationCacheHeader, "HIT") - } else { - w.Header().Set(NormalizationCacheHeader, "MISS") + s := func(hit bool) string { + if hit { + return "HIT" } + return "MISS" + } + + if h.enableNormalizationCacheResponseHeader { + w.Header().Set(NormalizationCacheHeader, s(opCtx.normalizationCacheHit)) + w.Header().Set(VariablesNormalizationCacheHeader, s(opCtx.variablesNormalizationCacheHit)) + w.Header().Set(VariablesRemappingCacheHeader, s(opCtx.variablesRemappingCacheHit)) } if h.enablePersistedOperationCacheResponseHeader { - if opCtx.persistedOperationCacheHit { - w.Header().Set(PersistedOperationCacheHeader, "HIT") - } else { - w.Header().Set(PersistedOperationCacheHeader, "MISS") - } + w.Header().Set(PersistedOperationCacheHeader, s(opCtx.persistedOperationCacheHit)) } if h.enableExecutionPlanCacheResponseHeader { - if opCtx.planCacheHit { - w.Header().Set(ExecutionPlanCacheHeader, "HIT") - } else { - w.Header().Set(ExecutionPlanCacheHeader, "MISS") - } + w.Header().Set(ExecutionPlanCacheHeader, s(opCtx.planCacheHit)) } } diff --git a/router/core/graphql_prehandler.go b/router/core/graphql_prehandler.go index bbc3473034..5a38941b6a 100644 --- a/router/core/graphql_prehandler.go +++ b/router/core/graphql_prehandler.go @@ -776,22 +776,14 @@ func (h *PreHandler) handleOperation(w http.ResponseWriter, req *http.Request, v return err } - // Set the cache hit attribute on the span engineNormalizeSpan.SetAttributes(otel.WgNormalizationCacheHit.Bool(cached)) - requestContext.operation.normalizationCacheHit = operationKit.parsedOperation.NormalizationCacheHit /** * Normalize the variables */ - // Normalize the variables returns list of uploads mapping if there are any of them present in a query - // type UploadPathMapping struct { - // VariableName string - is a variable name holding the direct or nested value of type Upload, example "f" - // OriginalUploadPath string - is a path relative to variables which have an Upload type, example "variables.f" - // NewUploadPath string - if variable was used in the inline object like this `arg: {f: $f}` this field will hold the new extracted path, example "variables.a.f", if it is an empty, there was no change in the path - // } - uploadsMapping, err := operationKit.NormalizeVariables() + cached, uploadsMapping, err := operationKit.NormalizeVariables() if err != nil { rtrace.AttachErrToSpan(engineNormalizeSpan, err) @@ -804,21 +796,24 @@ func (h *PreHandler) handleOperation(w http.ResponseWriter, req *http.Request, v } engineNormalizeSpan.End() - return err } + engineNormalizeSpan.SetAttributes(otel.WgVariablesNormalizationCacheHit.Bool(cached)) + requestContext.operation.variablesNormalizationCacheHit = cached - // update file uploads path if they were used in nested field in the extracted variables + // Update file upload paths if they were used in the nested field of the extracted variables. for mapping := range slices.Values(uploadsMapping) { - // if the NewUploadPath is empty it means that there was no change in the path - e.g. upload was directly passed to the argument - // e.g. field(fileArgument: $file) will result in []UploadPathMapping{ {VariableName: "file", OriginalUploadPath: "variables.file", NewUploadPath: ""} } + // If the NewUploadPath is empty, there was no change in the path: + // upload was directly passed to the argument. For example, "field(fileArgument: $file)" + // will result in uploadsMapping containing such an item: + // {VariableName: "file", OriginalUploadPath: "variables.file", NewUploadPath: ""} if mapping.NewUploadPath == "" { continue } - // look for the corresponding file which was used in the nested argument - // we are matching original upload path passed via uploads map with the mapping items + // Look for the corresponding file that was used in the nested argument. idx := slices.IndexFunc(requestContext.operation.files, func(file *httpclient.FileUpload) bool { + // Match upload path passed via slice of FileUpload with the mapping items. return file.VariablePath() == mapping.OriginalUploadPath }) @@ -826,16 +821,19 @@ func (h *PreHandler) handleOperation(w http.ResponseWriter, req *http.Request, v continue } - // if NewUploadPath is not empty the file argument was used in the nested object, and we need to update the path - // e.g. field(arg: {file: $file}) normalized to field(arg: $a) will result in []UploadPathMapping{ {VariableName: "file", OriginalUploadPath: "variables.file", NewUploadPath: "variables.a.file"} } - // so "variables.file" should be updated to "variables.a.file" + // If NewUploadPath is not empty, the file argument was used in the nested object, + // and we need to update the path. + // For example, "field(arg: {file: $file})" normalized to "field(arg: $a)" will result in + // uploadsMapping containing such an item: + // {VariableName: "file", OriginalUploadPath: "variables.file", NewUploadPath: "variables.a.file"} + // In short, "variables.file" should be updated to "variables.a.file". requestContext.operation.files[idx].SetVariablePath(uploadsMapping[idx].NewUploadPath) } - // RemapVariables is updating and sort variables name to be able to have them in a predictable order - // after remapping requestContext.operation.remapVariables map will contain new names as a keys and old names as a values - to be able to extract the old values - // because it does not rename variables in a variables json - err = operationKit.RemapVariables(h.disableVariablesRemapping) + // requestContext.operation.remapVariables map will contain new names as keys and + // old names as values - to be able to extract the old values. + // It does not rename variables in variables JSON. + cached, err = operationKit.RemapVariables(h.disableVariablesRemapping) if err != nil { rtrace.AttachErrToSpan(engineNormalizeSpan, err) @@ -848,10 +846,11 @@ func (h *PreHandler) handleOperation(w http.ResponseWriter, req *http.Request, v } engineNormalizeSpan.End() - return err } + engineNormalizeSpan.SetAttributes(otel.WgVariablesRemappingCacheHit.Bool(cached)) + requestContext.operation.variablesRemappingCacheHit = cached requestContext.operation.hash = operationKit.parsedOperation.ID requestContext.operation.internalHash = operationKit.parsedOperation.InternalID requestContext.operation.remapVariables = operationKit.parsedOperation.RemapVariables diff --git a/router/core/operation_processor.go b/router/core/operation_processor.go index 8e91a9fc77..3d171f5123 100644 --- a/router/core/operation_processor.go +++ b/router/core/operation_processor.go @@ -47,27 +47,33 @@ var ( type ParsedOperation struct { // ID represents a unique-ish ID for the operation calculated by hashing - // its normalized representation + // its normalized representation. ID uint64 + // InternalID is the internal ID of the operation calculated by hashing - // its normalized representation with the original operation name and normalized variables + // its normalized representation with the original operation name and normalized variables. InternalID uint64 - // Sha256Hash is the sha256 hash of the original operation query sent by the client + + // Sha256Hash is the sha256 hash of the original operation query sent by the client. Sha256Hash string - // Type is a string representing the operation type. One of - // "query", "mutation", "subscription" + + // Type is a string representing the operation type. One of "query", "mutation", "subscription". Type string Variables *fastjson.Object RemapVariables map[string]string - // NormalizedRepresentation is the normalized representation of the operation - // as a string. This is provided for modules to be able to access the - // operation. Only available after the operation has been normalized. - NormalizedRepresentation string + + // NormalizedRepresentation is the normalized representation of the operation as a string. + // This is provided for modules to be able to access the operation. + // Only available after the operation has been normalized. + NormalizedRepresentation string + Request GraphQLRequest GraphQLRequestExtensions GraphQLRequestExtensions IsPersistedOperation bool PersistedOperationCacheHit bool - // NormalizationCacheHit is set to true if the request is a non-persisted operation and the normalized operation was loaded from cache + + // NormalizationCacheHit is set to true if the request is a non-persisted operation, + // and the normalized operation was loaded from cache. NormalizationCacheHit bool } @@ -107,6 +113,8 @@ type OperationProcessorOptions struct { PersistedOpsNormalizationCache *ristretto.Cache[uint64, NormalizationCacheEntry] NormalizationCache *ristretto.Cache[uint64, NormalizationCacheEntry] QueryDepthCache *ristretto.Cache[uint64, ComplexityCacheEntry] + VariablesNormalizationCache *ristretto.Cache[uint64, VariablesNormalizationCacheEntry] + RemapVariablesCache *ristretto.Cache[uint64, RemapVariablesCacheEntry] ValidationCache *ristretto.Cache[uint64, bool] OperationHashCache *ristretto.Cache[uint64, string] ParseKitPoolSize int @@ -160,6 +168,8 @@ type OperationCache struct { persistedOperationNormalizationCache *ristretto.Cache[uint64, NormalizationCacheEntry] normalizationCache *ristretto.Cache[uint64, NormalizationCacheEntry] + variablesNormalizationCache *ristretto.Cache[uint64, VariablesNormalizationCacheEntry] + remapVariablesCache *ristretto.Cache[uint64, RemapVariablesCacheEntry] complexityCache *ristretto.Cache[uint64, ComplexityCacheEntry] validationCache *ristretto.Cache[uint64, bool] operationHashCache *ristretto.Cache[uint64, string] @@ -749,12 +759,39 @@ func (o *OperationKit) normalizePersistedOperation(clientName string, isApq bool } type NormalizationCacheEntry struct { - operationID uint64 normalizedRepresentation string operationType string operationDefinitionRef int } +type VariablesNormalizationCacheEntry struct { + // See ParsedOperation for the explanation of the fields below. + normalizedRepresentation string + id uint64 + + // variables store JSON of the normalized variables. + variables json.RawMessage + + // The uploadsMapping slice tracks file upload variables and how their paths change during + // GraphQL operation normalization. This is specifically for handling the GraphQL multipart + // request spec for file uploads. + uploadsMapping []uploads.UploadPathMapping + + // reparse indicates whether the operation document needs to be reparsed from + // its string representation when retrieved from the cache. + reparse bool +} + +type RemapVariablesCacheEntry struct { + // internalID is used as the cache key for validation, complexity and planner caches. + internalID uint64 + + normalizedRepresentation string + + // result of remapping + remapVariables map[string]string +} + type ComplexityCacheEntry struct { Depth int TotalFields int @@ -772,6 +809,13 @@ func (o *OperationKit) normalizeNonPersistedOperation() (cached bool, err error) o.parsedOperation.NormalizedRepresentation = entry.normalizedRepresentation o.parsedOperation.Type = entry.operationType o.parsedOperation.NormalizationCacheHit = true + + // remove skip include variables from variables + // as they were removed during normalization, but still present when we get operation from cache + for _, varName := range skipIncludeVariableNames { + o.parsedOperation.Request.Variables = jsonparser.Delete(o.parsedOperation.Request.Variables, varName) + } + err = o.setAndParseOperationDoc() if err != nil { return false, err @@ -790,15 +834,9 @@ func (o *OperationKit) normalizeNonPersistedOperation() (cached bool, err error) } } - // reset with the original variables + // set variables to the normalized variables as skip inlude variables will be removed after normalization o.parsedOperation.Request.Variables = o.kit.doc.Input.Variables - // Hash the normalized operation with the static operation name & original variables 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("normalizeNonPersistedOperation (uncached) failed generating operation hash: %w", err)) - } - // 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) @@ -811,7 +849,6 @@ func (o *OperationKit) normalizeNonPersistedOperation() (cached bool, err error) if o.cache != nil && o.cache.normalizationCache != nil { entry := NormalizationCacheEntry{ - operationID: o.parsedOperation.InternalID, normalizedRepresentation: o.parsedOperation.NormalizedRepresentation, operationType: o.parsedOperation.Type, } @@ -840,7 +877,34 @@ func (o *OperationKit) setAndParseOperationDoc() error { return nil } -func (o *OperationKit) NormalizeVariables() ([]uploads.UploadPathMapping, error) { +func (o *OperationKit) normalizeVariablesCacheKey() uint64 { + _, _ = o.kit.keyGen.Write(o.kit.doc.Input.Variables) + _, _ = o.kit.keyGen.WriteString(o.parsedOperation.NormalizedRepresentation) + + sum := o.kit.keyGen.Sum64() + o.kit.keyGen.Reset() + return sum +} + +// NormalizeVariables returns a slice of upload mappings if there are any of them present in a query. +func (o *OperationKit) NormalizeVariables() (cached bool, mapping []uploads.UploadPathMapping, err error) { + cacheKey := o.normalizeVariablesCacheKey() + if o.cache != nil && o.cache.variablesNormalizationCache != nil { + entry, ok := o.cache.variablesNormalizationCache.Get(cacheKey) + if ok { + o.parsedOperation.NormalizedRepresentation = entry.normalizedRepresentation + o.parsedOperation.ID = entry.id + o.parsedOperation.Request.Variables = entry.variables + + if entry.reparse { + if err = o.setAndParseOperationDoc(); err != nil { + return false, nil, err + } + } + return true, entry.uploadsMapping, nil + } + } + variablesBefore := make([]byte, len(o.kit.doc.Input.Variables)) copy(variablesBefore, o.kit.doc.Input.Variables) @@ -850,9 +914,7 @@ func (o *OperationKit) NormalizeVariables() ([]uploads.UploadPathMapping, error) report := &operationreport.Report{} uploadsMapping := o.kit.variablesNormalizer.NormalizeOperation(o.kit.doc, o.operationProcessor.executor.ClientSchema, report) if report.HasErrors() { - return nil, &reportError{ - report: report, - } + return false, nil, &reportError{report: report} } // Assuming the user sends a multi-operation document @@ -872,40 +934,85 @@ func (o *OperationKit) NormalizeVariables() ([]uploads.UploadPathMapping, error) 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) + err = o.kit.printer.Print(o.kit.doc, o.kit.normalizedOperation) if err != nil { - return nil, err + return false, nil, 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 nil, err + return false, nil, err } - o.parsedOperation.ID = o.kit.keyGen.Sum64() + o.kit.keyGen.Reset() // If the normalized form of the operation didn't change, we don't need to print it again if bytes.Equal(o.kit.doc.Input.Variables, variablesBefore) && bytes.Equal(o.kit.doc.Input.RawBytes, operationRawBytesBefore) { - return uploadsMapping, nil + if o.cache != nil && o.cache.variablesNormalizationCache != nil { + entry := VariablesNormalizationCacheEntry{ + uploadsMapping: uploadsMapping, + id: o.parsedOperation.ID, + normalizedRepresentation: o.parsedOperation.NormalizedRepresentation, + variables: o.parsedOperation.Request.Variables, + reparse: false, + } + o.cache.variablesNormalizationCache.Set(cacheKey, entry, 1) + } + + return false, uploadsMapping, nil } o.kit.normalizedOperation.Reset() err = o.kit.printer.Print(o.kit.doc, o.kit.normalizedOperation) if err != nil { - return nil, err + return false, nil, err } o.parsedOperation.NormalizedRepresentation = o.kit.normalizedOperation.String() o.parsedOperation.Request.Variables = o.kit.doc.Input.Variables - return uploadsMapping, nil + if o.cache != nil && o.cache.variablesNormalizationCache != nil { + entry := VariablesNormalizationCacheEntry{ + uploadsMapping: uploadsMapping, + id: o.parsedOperation.ID, + normalizedRepresentation: o.parsedOperation.NormalizedRepresentation, + variables: o.parsedOperation.Request.Variables, + reparse: true, + } + o.cache.variablesNormalizationCache.Set(cacheKey, entry, 1) + } + + return false, uploadsMapping, nil } -func (o *OperationKit) RemapVariables(disabled bool) error { +func (o *OperationKit) remapVariablesCacheKey() uint64 { + _, _ = o.kit.keyGen.WriteString(o.parsedOperation.NormalizedRepresentation) + sum := o.kit.keyGen.Sum64() + o.kit.keyGen.Reset() + return sum +} + +// RemapVariables updates and sorts variable names to have them in a predictable order. +func (o *OperationKit) RemapVariables(disabled bool) (cached bool, err error) { + cacheKey := o.remapVariablesCacheKey() + if o.cache != nil && o.cache.remapVariablesCache != nil { + entry, ok := o.cache.remapVariablesCache.Get(cacheKey) + if ok { + o.parsedOperation.NormalizedRepresentation = entry.normalizedRepresentation + o.parsedOperation.InternalID = entry.internalID + o.parsedOperation.RemapVariables = entry.remapVariables + + if err := o.setAndParseOperationDoc(); err != nil { + return false, err + } + + return true, nil + } + } + report := &operationreport.Report{} // even if the variables are disabled, we still need to execute rest of the method, @@ -913,9 +1020,7 @@ func (o *OperationKit) RemapVariables(disabled bool) error { if !disabled { variablesMap := o.kit.variablesRemapper.NormalizeOperation(o.kit.doc, o.operationProcessor.executor.ClientSchema, report) if report.HasErrors() { - return &reportError{ - report: report, - } + return false, &reportError{report: report} } o.parsedOperation.RemapVariables = variablesMap } @@ -930,19 +1035,17 @@ func (o *OperationKit) RemapVariables(disabled bool) error { 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) + err = o.kit.printer.Print(o.kit.doc, o.kit.normalizedOperation) if err != nil { - return errors.WithStack(fmt.Errorf("RemapVariables failed generating operation hash: %w", err)) + return false, errors.WithStack(fmt.Errorf("RemapVariables failed generating operation hash: %w", 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 + return false, err } - // Generate the operation ID o.parsedOperation.InternalID = o.kit.keyGen.Sum64() o.kit.keyGen.Reset() @@ -950,12 +1053,21 @@ func (o *OperationKit) RemapVariables(disabled bool) error { o.kit.normalizedOperation.Reset() err = o.kit.printer.Print(o.kit.doc, o.kit.normalizedOperation) if err != nil { - return err + return false, err } o.parsedOperation.NormalizedRepresentation = o.kit.normalizedOperation.String() - return nil + if o.cache != nil && o.cache.remapVariablesCache != nil { + entry := RemapVariablesCacheEntry{ + internalID: o.parsedOperation.InternalID, + normalizedRepresentation: o.parsedOperation.NormalizedRepresentation, + remapVariables: o.parsedOperation.RemapVariables, + } + o.cache.remapVariablesCache.Set(cacheKey, entry, 1) + } + + return false, nil } func (o *OperationKit) loadPersistedOperationFromCache(clientName string) (ok bool, includeOpName bool, err error) { @@ -995,7 +1107,6 @@ func (o *OperationKit) handleFoundPersistedOperationEntry(entry NormalizationCac // as we skip parse for the cached persisted operations o.parsedOperation.IsPersistedOperation = true o.parsedOperation.NormalizationCacheHit = true - o.parsedOperation.InternalID = entry.operationID o.parsedOperation.NormalizedRepresentation = entry.normalizedRepresentation o.parsedOperation.Type = entry.operationType // We will always only have a single operation definition in the document @@ -1042,7 +1153,6 @@ func (o *OperationKit) persistedOperationCacheKeyHasTtl(clientName string, inclu func (o *OperationKit) savePersistedOperationToCache(clientName string, isApq bool, skipIncludeVariableNames []string) { cacheKey := o.generatePersistedOperationCacheKey(clientName, skipIncludeVariableNames, o.kit.numOperations > 1) entry := NormalizationCacheEntry{ - operationID: o.parsedOperation.InternalID, normalizedRepresentation: o.parsedOperation.NormalizedRepresentation, operationType: o.parsedOperation.Type, operationDefinitionRef: o.operationDefinitionRef, @@ -1319,12 +1429,15 @@ func NewOperationProcessor(opts OperationProcessorOptions) *OperationProcessor { processor.parseKitSemaphore <- i processor.parseKits[i] = createParseKit(i, processor.parseKitOptions) } - if opts.NormalizationCache != nil || opts.ValidationCache != nil || opts.QueryDepthCache != nil || opts.OperationHashCache != nil || opts.EnablePersistedOperationsCache { + if opts.NormalizationCache != nil || opts.ValidationCache != nil || opts.QueryDepthCache != nil || opts.OperationHashCache != nil || opts.EnablePersistedOperationsCache || + opts.VariablesNormalizationCache != nil || opts.RemapVariablesCache != nil { processor.operationCache = &OperationCache{ - normalizationCache: opts.NormalizationCache, - validationCache: opts.ValidationCache, - complexityCache: opts.QueryDepthCache, - operationHashCache: opts.OperationHashCache, + normalizationCache: opts.NormalizationCache, + validationCache: opts.ValidationCache, + complexityCache: opts.QueryDepthCache, + operationHashCache: opts.OperationHashCache, + variablesNormalizationCache: opts.VariablesNormalizationCache, + remapVariablesCache: opts.RemapVariablesCache, } } if opts.EnablePersistedOperationsCache { diff --git a/router/core/operation_processor_test.go b/router/core/operation_processor_test.go index ba63cfe899..589fcdf50c 100644 --- a/router/core/operation_processor_test.go +++ b/router/core/operation_processor_test.go @@ -261,7 +261,7 @@ func TestNormalizeVariablesOperationProcessor(t *testing.T) { _, err = kit.NormalizeOperation("test", false) require.NoError(t, err) - _, err = kit.NormalizeVariables() + _, _, err = kit.NormalizeVariables() require.NoError(t, err) assert.Equal(t, tc.ExpectedNormalizedRepresentation, kit.parsedOperation.NormalizedRepresentation) diff --git a/router/core/websocket.go b/router/core/websocket.go index 06ee4adc55..c662fa1ba0 100644 --- a/router/core/websocket.go +++ b/router/core/websocket.go @@ -903,18 +903,21 @@ func (h *WebSocketConnectionHandler) parseAndPlan(registration *SubscriptionRegi opContext.normalizationTime = time.Since(startNormalization) return nil, nil, err } - opContext.normalizationCacheHit = operationKit.parsedOperation.NormalizationCacheHit - if _, err := operationKit.NormalizeVariables(); err != nil { + cached, _, err := operationKit.NormalizeVariables() + if err != nil { opContext.normalizationTime = time.Since(startNormalization) return nil, nil, err } + opContext.variablesNormalizationCacheHit = cached - if err := operationKit.RemapVariables(h.disableVariablesRemapping); err != nil { + cached, err = operationKit.RemapVariables(h.disableVariablesRemapping) + if err != nil { opContext.normalizationTime = time.Since(startNormalization) return nil, nil, err } + opContext.variablesRemappingCacheHit = cached opContext.hash = operationKit.parsedOperation.ID opContext.internalHash = operationKit.parsedOperation.InternalID diff --git a/router/pkg/otel/attributes.go b/router/pkg/otel/attributes.go index 1a1a0d5baf..37807fbf75 100644 --- a/router/pkg/otel/attributes.go +++ b/router/pkg/otel/attributes.go @@ -36,6 +36,8 @@ const ( WgFeatureFlag = attribute.Key("wg.feature_flag") WgAcquireResolverWaitTimeMs = attribute.Key("wg.engine.resolver.wait_time_ms") WgNormalizationCacheHit = attribute.Key("wg.engine.normalization_cache_hit") + WgVariablesNormalizationCacheHit = attribute.Key("wg.engine.variables_normalization_cache_hit") + WgVariablesRemappingCacheHit = attribute.Key("wg.engine.variables_remapping_cache_hit") WgValidationCacheHit = attribute.Key("wg.engine.validation_cache_hit") WgVariablesValidationSkipped = attribute.Key("wg.engine.variables_validation_skipped") WgQueryDepth = attribute.Key("wg.operation.complexity.query_depth")