From b11d760b21a54ae5e067b3c5a9d5a13be73885f9 Mon Sep 17 00:00:00 2001 From: Daniel Hartnell Date: Thu, 19 Dec 2024 09:36:15 -0800 Subject: [PATCH 1/6] Introduce hook to monitor cache hits --- lib/graphql/fragment_cache/fragment.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/graphql/fragment_cache/fragment.rb b/lib/graphql/fragment_cache/fragment.rb index ea7ab86..ea5b889 100644 --- a/lib/graphql/fragment_cache/fragment.rb +++ b/lib/graphql/fragment_cache/fragment.rb @@ -30,6 +30,15 @@ def read_multi(fragments) FragmentCache.cache_store.read_multi(*cache_keys) end + fragments.map do |fragment| + cache_lookup_event( + cache_key: fragment.cache_key, + operation_name: fragment.context.query.operation_name, + path: fragment.path, + cache_hit: cache_keys_to_values.key?(fragment.cache_key), + ) + end + # Fragmenst without values or with renew_cache: true in their context will have nil values like the read method fragments_to_cache_keys .map { |fragment, cache_key| [fragment, cache_keys_to_values[cache_key]] }.to_h @@ -87,6 +96,11 @@ def interpreter_context def final_value @final_value ||= context.query.result["data"] end + + def cache_lookup_event(cache_key, operation_name, path, cache_hit) + # This method can be implemented in your application + # This provides a mechanism to monitor cache hits for a fragment + end end end end From 11c1228fd73708c5202837aa8ac260c82e44d906 Mon Sep 17 00:00:00 2001 From: Daniel Hartnell Date: Thu, 19 Dec 2024 09:53:25 -0800 Subject: [PATCH 2/6] Fixes failing specs when query context is missing --- lib/graphql/fragment_cache/fragment.rb | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/graphql/fragment_cache/fragment.rb b/lib/graphql/fragment_cache/fragment.rb index ea5b889..efb31a1 100644 --- a/lib/graphql/fragment_cache/fragment.rb +++ b/lib/graphql/fragment_cache/fragment.rb @@ -30,13 +30,17 @@ def read_multi(fragments) FragmentCache.cache_store.read_multi(*cache_keys) end - fragments.map do |fragment| - cache_lookup_event( - cache_key: fragment.cache_key, - operation_name: fragment.context.query.operation_name, - path: fragment.path, - cache_hit: cache_keys_to_values.key?(fragment.cache_key), - ) + begin + fragments.map do |fragment| + cache_lookup_event( + cache_key: fragment.cache_key, + operation_name: fragment.context.query.operation_name, + path: fragment.path, + cache_hit: cache_keys_to_values.key?(fragment.cache_key), + ) + end + rescue + # Allow cache_lookup_event to fail when we do not have the data we need end # Fragmenst without values or with renew_cache: true in their context will have nil values like the read method From bfba37b6596d12bf8cda813eead42c99872b6404 Mon Sep 17 00:00:00 2001 From: Daniel Hartnell Date: Thu, 19 Dec 2024 12:50:34 -0800 Subject: [PATCH 3/6] Support any number of named arguments --- lib/graphql/fragment_cache/fragment.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/graphql/fragment_cache/fragment.rb b/lib/graphql/fragment_cache/fragment.rb index efb31a1..1f917b6 100644 --- a/lib/graphql/fragment_cache/fragment.rb +++ b/lib/graphql/fragment_cache/fragment.rb @@ -40,7 +40,7 @@ def read_multi(fragments) ) end rescue - # Allow cache_lookup_event to fail when we do not have the data we need + # Allow cache_lookup_event to fail when we do not have all of the requested attributes end # Fragmenst without values or with renew_cache: true in their context will have nil values like the read method @@ -101,7 +101,7 @@ def final_value @final_value ||= context.query.result["data"] end - def cache_lookup_event(cache_key, operation_name, path, cache_hit) + def cache_lookup_event(**args) # This method can be implemented in your application # This provides a mechanism to monitor cache hits for a fragment end From 1e33358604339c3139c81d9bfbc342f99d06de27 Mon Sep 17 00:00:00 2001 From: Daniel Hartnell Date: Mon, 30 Dec 2024 09:35:29 -0800 Subject: [PATCH 4/6] fix: address rubocop violation --- lib/graphql/fragment_cache/fragment.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/graphql/fragment_cache/fragment.rb b/lib/graphql/fragment_cache/fragment.rb index 1f917b6..bc1d67e 100644 --- a/lib/graphql/fragment_cache/fragment.rb +++ b/lib/graphql/fragment_cache/fragment.rb @@ -36,7 +36,7 @@ def read_multi(fragments) cache_key: fragment.cache_key, operation_name: fragment.context.query.operation_name, path: fragment.path, - cache_hit: cache_keys_to_values.key?(fragment.cache_key), + cache_hit: cache_keys_to_values.key?(fragment.cache_key) ) end rescue From b6ed41165af44c395c7be35a7914569f1134bb36 Mon Sep 17 00:00:00 2001 From: Daniel Hartnell Date: Mon, 30 Dec 2024 11:33:56 -0800 Subject: [PATCH 5/6] Introduce option to enable cache lookup monitoring --- lib/graphql/fragment_cache.rb | 2 ++ lib/graphql/fragment_cache/fragment.rb | 22 ++++++++++++---------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/graphql/fragment_cache.rb b/lib/graphql/fragment_cache.rb index 710e7a9..ab8e100 100644 --- a/lib/graphql/fragment_cache.rb +++ b/lib/graphql/fragment_cache.rb @@ -25,6 +25,7 @@ module FragmentCache class << self attr_reader :cache_store attr_accessor :enabled + attr_accessor :monitoring_enabled attr_accessor :namespace attr_accessor :default_options @@ -87,6 +88,7 @@ def verify_interpreter_and_analysis!(schema_defn) self.cache_store = MemoryStore.new self.enabled = true + self.monitoring_enabled = false self.namespace = "graphql" self.default_options = {} self.skip_cache_when_query_has_errors = false diff --git a/lib/graphql/fragment_cache/fragment.rb b/lib/graphql/fragment_cache/fragment.rb index bc1d67e..fcae4a8 100644 --- a/lib/graphql/fragment_cache/fragment.rb +++ b/lib/graphql/fragment_cache/fragment.rb @@ -30,17 +30,19 @@ def read_multi(fragments) FragmentCache.cache_store.read_multi(*cache_keys) end - begin - fragments.map do |fragment| - cache_lookup_event( - cache_key: fragment.cache_key, - operation_name: fragment.context.query.operation_name, - path: fragment.path, - cache_hit: cache_keys_to_values.key?(fragment.cache_key) - ) + if GraphQL::FragmentCache.monitoring_enabled + begin + fragments.map do |fragment| + cache_lookup_event( + cache_key: fragment.cache_key, + operation_name: fragment.context.query.operation_name, + path: fragment.path, + cache_hit: cache_keys_to_values.key?(fragment.cache_key) + ) + end + rescue + # Allow cache_lookup_event to fail when we do not have all of the requested attributes end - rescue - # Allow cache_lookup_event to fail when we do not have all of the requested attributes end # Fragmenst without values or with renew_cache: true in their context will have nil values like the read method From ceefe426e59f67aec5acba155e35b680b182a3bd Mon Sep 17 00:00:00 2001 From: Daniel Hartnell Date: Mon, 30 Dec 2024 11:44:00 -0800 Subject: [PATCH 6/6] Adds documentation for cache monitoring --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index faa93ab..ecce0a4 100644 --- a/README.md +++ b/README.md @@ -446,6 +446,30 @@ Cache processing can be disabled if needed. For example: GraphQL::FragmentCache.enabled = false if Rails.env.test? ``` +## Cache lookup monitoring + +It may be useful to capture cache lookup events. When monitoring is enabled, the `cache_key`, `operation_name`, `path` and a boolean indicating a cache hit or miss will be sent to a `cache_lookup_event` method. This method can be implemented in your application to handle the event. + +Example handler defined in a Rails initializer: + +```ruby +module GraphQL + module FragmentCache + class Fragment + def self.cache_lookup_event(**args) + # Monitoring such as incrementing a cache hit counter metric + end + end + end +end +``` + +Like managing caching itself, monitoring can be enabled if needed. It is disabled by default. For example: + +```ruby +GraphQL::FragmentCache.monitoring_enabled = true +``` + ## Limitations 1. `Schema#execute`, [graphql-batch](https://github.com/Shopify/graphql-batch) and _graphql-ruby-fragment_cache_ do not [play well](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/issues/45) together. The problem appears when `cache_fragment` is _inside_ the `.then` block: